Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
bc5a8b9
Upgrade to Elixir 1.18.4, Erlang 27.3.4.6, Node.js 22.21.1
tristanperalta Jan 17, 2026
92c7efe
Upgrade Hex dependencies to latest compatible versions
tristanperalta Jan 17, 2026
63d2d0c
Upgrade cowlib, gun, and related dependencies
tristanperalta Jan 17, 2026
82d4d57
Fix credo --strict issues and add contributing guide
tristanperalta Jan 17, 2026
553cf26
Add precommit task and fix dialyzer errors
tristanperalta Jan 17, 2026
5aee2e4
fix!: Add error handling to Browser.new_page/2 and fix related issues
tristanperalta Jan 17, 2026
45b8a72
Add feature parity tracking document and ignore .claude directory
tristanperalta Jan 17, 2026
41853d3
feat: Add navigation methods go_back, go_forward, wait_for_url
tristanperalta Jan 17, 2026
def4f12
Implement Dialog module for browser dialog handling
tristanperalta Jan 17, 2026
bb248ca
Add getByRole, getByTestId, getByLabel locator methods
tristanperalta Jan 17, 2026
a7b9fc0
Remove broken documentation cross-references in Locator module
tristanperalta Jan 17, 2026
de85139
Remove broken documentation cross-references across modules
tristanperalta Jan 17, 2026
baa8939
Add wait_for_navigation to Frame and Page modules
tristanperalta Jan 17, 2026
3264708
Add Locator.filter/2 for filtering locators by conditions
tristanperalta Jan 17, 2026
332dad8
Add BrowserContext.storage_state/1-2 for session persistence
tristanperalta Jan 17, 2026
9a7287a
Add BrowserContext.set_geolocation/2 for mocking browser location
tristanperalta Jan 17, 2026
2b57d1f
Add Download module with save_as/path for file downloads
tristanperalta Jan 17, 2026
acc18b0
Add Mouse module for virtual mouse interactions
tristanperalta Jan 17, 2026
57db1c0
Add FrameLocator module for navigating into iframes
tristanperalta Jan 17, 2026
9ae7584
Add Page.pdf/2 for generating PDFs from pages
tristanperalta Jan 17, 2026
cf4cd06
Add Tracing module for recording browser traces
tristanperalta Jan 17, 2026
a129d57
Add getByPlaceholder, getByAltText, getByTitle locator methods
tristanperalta Jan 17, 2026
07a803c
Add query methods to Page module
tristanperalta Jan 17, 2026
2a3acd3
Add Page.bring_to_front/1 and Page.viewport_size/1 methods
tristanperalta Jan 17, 2026
d5633a8
Add Locator.and_/2 to combine locators with AND logic
tristanperalta Jan 17, 2026
943a2e1
Add FileChooser module for handling file input dialogs
tristanperalta Jan 17, 2026
d0ebc77
Add Locator.press_sequentially/3 for typing text character by character
tristanperalta Jan 17, 2026
40f1256
Add Frame hierarchy methods: page, parent_frame, child_frames, is_det…
tristanperalta Jan 17, 2026
1733734
Add Page.check, uncheck, set_checked, and set_input_files methods
tristanperalta Jan 17, 2026
1a7ce3c
Add Locator.frame_locator/2 and Locator.content_frame/1 for iframe na…
tristanperalta Jan 17, 2026
89aff5c
Add Locator.page/1 and Locator.highlight/1 methods
tristanperalta Jan 17, 2026
90df930
Add Page.wait_for_request and Page.wait_for_response for network waiting
tristanperalta Jan 17, 2026
a7143c8
Add Browser.is_connected/1 and Browser.browser_type/1 methods
tristanperalta Jan 17, 2026
38bb1ac
Add Page.frame/2 to find frames by name or URL
tristanperalta Jan 18, 2026
366ed68
Add Page.emulate_media/2 for CSS media feature emulation
tristanperalta Jan 18, 2026
9412d1e
Add Page.opener, Frame.frame_element, and Touchscreen.tap
tristanperalta Jan 18, 2026
7d8438d
Add Clock module for time manipulation in tests
tristanperalta Jan 18, 2026
11884f7
Add Page.add_script_tag/2 and Page.add_style_tag/2 methods
tristanperalta Jan 18, 2026
74a4f39
Add Page.eval_on_selector_all/4 for evaluating JS on all matching ele…
tristanperalta Jan 18, 2026
705fdd6
Add timeout, HTTP headers, and unroute_all methods
tristanperalta Jan 18, 2026
46b52be
Add Browser.start_tracing and Browser.stop_tracing for Chromium
tristanperalta Jan 18, 2026
e752ae0
Add Route.abort/2 and Locator.aria_snapshot/2 methods
tristanperalta Jan 18, 2026
e485753
Add Page.video/1 and Video module for video recordings
tristanperalta Jan 18, 2026
d2f07f0
Add BrowserContext.set_http_credentials/2 method
tristanperalta Jan 18, 2026
3646863
Add Browser.new_browser_cdp_session and context worker methods
tristanperalta Jan 18, 2026
1bad14d
Add Page.add_locator_handler and Page.remove_locator_handler methods
tristanperalta Jan 18, 2026
0c24bc9
Add Page.route_web_socket and BrowserContext.route_web_socket methods
tristanperalta Jan 18, 2026
a998dd8
Add Coverage module for JS and CSS code coverage collection
tristanperalta Jan 18, 2026
4b8c3c7
Add Page.console_messages/1 and Page.page_errors/1 methods
tristanperalta Jan 18, 2026
09351fe
Add Page.pause/1 and Page.request_gc/1 methods
tristanperalta Jan 18, 2026
1a83849
Add Page.tap/3 and Page.type/4 methods
tristanperalta Jan 18, 2026
6169572
Document drag_and_drop options for JS library compatibility
tristanperalta Jan 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .credo.exs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@
{Credo.Check.Readability.ModuleNames, []},
{Credo.Check.Readability.ParenthesesInCondition, []},
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
{Credo.Check.Readability.PredicateFunctionNames, []},
# Disabled: is_* functions match Playwright's JavaScript API for consistency
{Credo.Check.Readability.PredicateFunctionNames, false},
{Credo.Check.Readability.PreferImplicitTry, []},
{Credo.Check.Readability.RedundantBlankLines, []},
{Credo.Check.Readability.Semicolons, []},
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ jobs:
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: '1.16.2'
otp-version: '26.2.5'
elixir-version: '1.18.4'
otp-version: '27.3.4.6'
- name: Install Elixir dependencies
run: mix deps.get
- name: Install Playwright dependencies (e.g., browsers)
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ playwright-*.tar
.doctor.out
.vscode
*.iml
/.idea/
/.idea/
.claude/
4 changes: 4 additions & 0 deletions .mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[tools]
elixir = "1.18.4-otp-27"
erlang = "27.3.4.6"
nodejs = "22.21.1"
3 changes: 0 additions & 3 deletions .tool-versions

This file was deleted.

8 changes: 4 additions & 4 deletions lib/playwright.ex
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ defmodule Playwright do
| key/name | typ | | description |
| ----------| ----- | ----------- | ----------- |
| `client` | param | `client()` | The type of client (browser) to launch. |
| `options` | param | `options()` | `Playwright.SDK.Config.connect_options()` |
| `options` | param | `options()` | Connection options (see Config module) |
"""
@spec launch(client(), Config.connect_options() | map()) :: {:ok, Playwright.Browser.t()}
@spec connect(client(), map()) :: {:ok, Playwright.Browser.t()}
def connect(client, options \\ %{}) do
options = Map.merge(Config.connect_options(), options)
{:ok, session} = new_session(Playwright.SDK.Transport.WebSocket, options)
Expand All @@ -70,9 +70,9 @@ defmodule Playwright do
| key/name | typ | | description |
| ----------| ----- | ----------- | ----------- |
| `client` | param | `client()` | The type of client (browser) to launch. |
| `options` | param | `options()` | `Playwright.SDK.Config.launch_options()` |
| `options` | param | `options()` | Launch options (see Config module) |
"""
@spec launch(client(), Config.launch_options() | map()) :: {:ok, Playwright.Browser.t()}
@spec launch(client(), map()) :: {:ok, Playwright.Browser.t()}
def launch(client, options \\ %{}) do
options = Map.merge(Config.launch_options(), options)
{:ok, session} = new_session(Playwright.SDK.Transport.Driver, options)
Expand Down
4 changes: 2 additions & 2 deletions lib/playwright/api_request_context.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ defmodule Playwright.APIRequestContext do
# @spec patch(t(), binary(), options()) :: APIResponse.t()
# def patch(context, url, options \\ %{})

@spec post(t(), binary(), fetch_options()) :: Playwright.APIResponse.t()
@spec post(t(), binary(), fetch_options()) :: struct()
def post(%APIRequestContext{session: session} = context, url, options \\ %{}) do
Channel.post(
session,
Expand All @@ -64,7 +64,7 @@ defmodule Playwright.APIRequestContext do
# def storage_state(context, options \\ %{})

# TODO: move to `APIResponse.body`, probably.
@spec body(t(), Playwright.APIResponse.t()) :: any()
@spec body(t(), struct()) :: any()
def body(%APIRequestContext{session: session} = context, response) do
Channel.post(session, {:guid, context.guid}, :fetch_response_body, %{
fetchUid: response.fetchUid
Expand Down
64 changes: 64 additions & 0 deletions lib/playwright/artifact.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
defmodule Playwright.Artifact do
@moduledoc false
use Playwright.SDK.ChannelOwner

@doc """
Returns the path to the downloaded file after it has finished downloading.
"""
@spec path_after_finished(t()) :: binary() | {:error, term()}
def path_after_finished(%__MODULE__{session: session} = artifact) do
case Channel.post(session, {:guid, artifact.guid}, :path_after_finished) do
%{value: path} -> path
path when is_binary(path) -> path
{:error, _} = error -> error
end
end

@doc """
Saves the artifact to the specified path.
"""
@spec save_as(t(), binary()) :: :ok | {:error, term()}
def save_as(%__MODULE__{session: session} = artifact, path) do
case Channel.post(session, {:guid, artifact.guid}, :save_as, %{path: path}) do
{:ok, _} -> :ok
:ok -> :ok
{:error, _} = error -> error
end
end

@doc """
Returns the error message if the artifact failed to download, nil otherwise.
"""
@spec failure(t()) :: binary() | nil | {:error, term()}
def failure(%__MODULE__{session: session} = artifact) do
case Channel.post(session, {:guid, artifact.guid}, :failure) do
%{error: error} -> error
%{} -> nil
{:error, _} = error -> error
end
end

@doc """
Cancels the download.
"""
@spec cancel(t()) :: :ok | {:error, term()}
def cancel(%__MODULE__{session: session} = artifact) do
case Channel.post(session, {:guid, artifact.guid}, :cancel) do
{:ok, _} -> :ok
:ok -> :ok
{:error, _} = error -> error
end
end

@doc """
Deletes the downloaded file.
"""
@spec delete(t()) :: :ok | {:error, term()}
def delete(%__MODULE__{session: session} = artifact) do
case Channel.post(session, {:guid, artifact.guid}, :delete) do
{:ok, _} -> :ok
:ok -> :ok
{:error, _} = error -> error
end
end
end
150 changes: 128 additions & 22 deletions lib/playwright/browser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ defmodule Playwright.Browser do
- `:version`
"""
use Playwright.SDK.ChannelOwner
alias Playwright.{Browser, BrowserContext, Page}
alias Playwright.{Browser, BrowserContext, BrowserType, CDPSession, Page}
alias Playwright.SDK.{Channel, ChannelOwner, Extra}

@property :name
Expand All @@ -44,8 +44,32 @@ defmodule Playwright.Browser do
# API
# ---------------------------------------------------------------------------

# @spec browser_type(t()) :: BrowserType.t()
# def browser_type(browser)
@doc """
Returns the BrowserType that was used to launch this browser.

## Returns

- `BrowserType.t()`
"""
@spec browser_type(t()) :: BrowserType.t()
def browser_type(%Browser{parent: parent}), do: parent

@doc """
Returns whether the browser is still connected.

Returns `false` after the browser has been closed.

## Returns

- `boolean()`
"""
@spec is_connected(t()) :: boolean()
def is_connected(%Browser{session: session, guid: guid}) do
case Channel.find(session, {:guid, guid}, %{timeout: 100}) do
%Browser{} -> true
{:error, _} -> false
end
end

@doc """
Closes the browser.
Expand Down Expand Up @@ -94,9 +118,6 @@ defmodule Playwright.Browser do
Channel.list(browser.session, {:guid, browser.guid}, "BrowserContext")
end

# @spec is_connected(BrowserContext.t()) :: boolean()
# def is_connected(browser)

# @spec new_browser_cdp_session(BrowserContext.t()) :: Playwright.CDPSession.t()
# def new_browser_cdp_session(browser)

Expand Down Expand Up @@ -146,33 +167,118 @@ defmodule Playwright.Browser do
This is a convenience API function that should only be used for single-page
scenarios and short snippets. Production code and testing frameworks should
explicitly create via `Playwright.Browser.new_context/2` followed by
`Playwright.BrowserContext.new_page/2`, given the new context, to manage
`Playwright.BrowserContext.new_page/1`, given the new context, to manage
resource lifecycles.
"""
@spec new_page(t(), options()) :: Page.t()
@spec new_page(t(), options()) :: {:ok, Page.t()} | {:error, term()}
def new_page(browser, options \\ %{})

def new_page(%Browser{session: session} = browser, options) do
context = new_context(browser, options)
page = BrowserContext.new_page(context)
with context when is_struct(context) <- new_context(browser, options),
page when is_struct(page) <- BrowserContext.new_page(context) do
# establish co-dependency
Channel.patch(session, {:guid, context.guid}, %{owner_page: page})
Channel.patch(session, {:guid, page.guid}, %{owned_context: context})
{:ok, page}
end
end

# ---

@doc """
Start tracing for Chromium browser.

Records a trace that can be viewed in Chrome DevTools or Playwright Trace Viewer.

## Arguments

| key/name | type | description |
| -------------- | ------------- | ---------------------------------------- |
| `page` | `Page.t()` | Optional page to trace (default: all) |
| `:screenshots` | `boolean()` | Capture screenshots during trace |
| `:categories` | `[binary()]` | Trace categories to record |

## Returns

# TODO: handle the following, for `page`:
# ** (KeyError) key :guid not found in: {:error, %Playwright.Channel.Error{message: "Target closed"}}
- `:ok`

# establish co-dependency
Channel.patch(session, {:guid, context.guid}, %{owner_page: page})
Channel.patch(session, {:guid, page.guid}, %{owned_context: context})
## Example

Browser.start_tracing(browser)
# ... perform actions ...
trace = Browser.stop_tracing(browser)
File.write!("trace.json", trace)

## Note

Only supported on Chromium browsers.
"""
@spec start_tracing(t(), Page.t() | nil, options()) :: :ok
def start_tracing(%Browser{session: session, guid: guid}, page \\ nil, options \\ %{}) do
params = %{}
params = if page, do: Map.put(params, :page, %{guid: page.guid}), else: params
params = if options[:screenshots], do: Map.put(params, :screenshots, options[:screenshots]), else: params
params = if options[:categories], do: Map.put(params, :categories, options[:categories]), else: params

Channel.post(session, {:guid, guid}, :start_tracing, params)
:ok
end

# ---
@doc """
Stop tracing and return the trace data.

Returns the trace data as binary which can be saved to a file.

## Returns

- `binary()` - Trace data (JSON format, save as .json file)

## Example

# test_chromium_tracing.py
# @spec start_tracing(t(), Page.t(), options()) :: :ok
# def start_tracing(browser, page \\ nil, options \\ %{})
Browser.start_tracing(browser, page, %{screenshots: true})
Page.goto(page, "https://example.com")
trace = Browser.stop_tracing(browser)
File.write!("trace.json", trace)
"""
@spec stop_tracing(t()) :: binary()
def stop_tracing(%Browser{session: session, guid: guid}) do
artifact =
case Channel.post(session, {:guid, guid}, :stop_tracing, %{}) do
%{artifact: %{guid: artifact_guid}} ->
Channel.find(session, {:guid, artifact_guid})

%Playwright.Artifact{} = art ->
art
end

# Save to temp file, read contents, then clean up
temp_path = Path.join(System.tmp_dir!(), "pw_trace_#{:erlang.unique_integer([:positive])}.json")
Playwright.Artifact.save_as(artifact, temp_path)
data = File.read!(temp_path)
File.rm(temp_path)
Playwright.Artifact.delete(artifact)
data
end

# test_chromium_tracing.py
# @spec stop_tracing(t()) :: binary()
# def stop_tracing(browser)
@doc """
Creates a new CDP session attached to the browser.

This is Chromium-specific.

## Returns

- `CDPSession.t()`

## Example

session = Browser.new_browser_cdp_session(browser)
# Use CDP commands...
CDPSession.detach(session)
"""
@spec new_browser_cdp_session(t()) :: CDPSession.t()
def new_browser_cdp_session(%Browser{session: session, guid: guid}) do
Channel.post(session, {:guid, guid}, "newBrowserCDPSession")
end

# @spec version(BrowserContext.t()) :: binary
# def version(browser)
Expand Down
Loading