Created
March 16, 2026 20:27
-
-
Save ityonemo/1a4d5ba9d15a9c89bd2ee4acf76fccb9 to your computer and use it in GitHub Desktop.
Elixir - Async playwright "integration" testing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| defmodule Integration.Playwright do | |
| @moduledoc """ | |
| Runs Playwright scripts with sandbox metadata injection using MuonTrap. | |
| MuonTrap ensures Node/Playwright processes are killed when tests exit. | |
| ## Usage | |
| Include `userId` in the context to automatically authenticate before running the script: | |
| Playwright.run!(~S\""" | |
| await page.goto('/notes'); | |
| await expect(page.locator('h1')).toContainText('Notes'); | |
| \""", %{userId: user.id}) | |
| Additional context values are available in scripts as `context.keyName`: | |
| Playwright.run!(~S\""" | |
| await page.goto('/notes/' + context.noteId); | |
| await expect(page.locator('.note-title')).toContainText(context.title); | |
| \""", %{userId: user.id, noteId: note.id, title: note.title}) | |
| """ | |
| @base_url "http://127.0.0.1:4002" | |
| @runner_path Path.expand("../integration/playwright/runner.js", __DIR__) | |
| @timeout 30_000 | |
| defp wait_for_server(attempts \\ 50) do | |
| case :gen_tcp.connect(~c"127.0.0.1", 4002, [], 100) do | |
| {:ok, sock} -> | |
| :gen_tcp.close(sock) | |
| :ok | |
| {:error, _} when attempts > 0 -> | |
| Process.sleep(100) | |
| wait_for_server(attempts - 1) | |
| {:error, reason} -> | |
| raise "Server not available on port 4002: #{inspect(reason)}" | |
| end | |
| end | |
| # Find NODE_PATH for playwright (checks npx cache if not in global modules) | |
| defp node_path do | |
| case System.cmd("npm", ["root", "-g"], stderr_to_stdout: true) do | |
| {global_path, 0} -> | |
| global_path = String.trim(global_path) | |
| if File.exists?(Path.join(global_path, "playwright")) do | |
| global_path | |
| else | |
| find_npx_playwright_path() | |
| end | |
| _ -> | |
| find_npx_playwright_path() | |
| end | |
| end | |
| defp find_npx_playwright_path do | |
| npm_cache = System.get_env("npm_config_cache") || Path.expand("~/.npm") | |
| npx_dir = Path.join(npm_cache, "_npx") | |
| if File.dir?(npx_dir) do | |
| npx_dir | |
| |> File.ls!() | |
| |> Enum.find_value(fn subdir -> | |
| node_modules = Path.join([npx_dir, subdir, "node_modules"]) | |
| playwright_path = Path.join(node_modules, "playwright") | |
| if File.exists?(playwright_path), do: node_modules | |
| end) | |
| end | |
| end | |
| @doc """ | |
| Runs a Playwright script with the given context. | |
| The script has access to: | |
| - `page` - The Playwright Page object | |
| - `context` - The context map passed from Elixir | |
| - `expect` - Playwright's expect function for assertions | |
| If `userId` is present in the context, the script will automatically | |
| navigate to `/login_as/:id` first to authenticate as that user. | |
| Raises on failure with the error message from Playwright. | |
| """ | |
| def run!(script, context \\ %{}) do | |
| script = maybe_prepend_login(script, context) | |
| # Ensure server is ready before running Playwright | |
| wait_for_server() | |
| sandbox_metadata = Process.get(:sandbox_metadata) | |
| payload = %{ | |
| script: script, | |
| context: context, | |
| baseUrl: @base_url, | |
| sandboxMetadata: sandbox_metadata | |
| } | |
| json_payload = Jason.encode!(payload) | |
| # Write payload to temp file (MuonTrap doesn't support stdin) | |
| tmp_path = Path.join(System.tmp_dir!(), "playwright_#{:erlang.unique_integer([:positive])}.json") | |
| File.write!(tmp_path, json_payload) | |
| try do | |
| env = | |
| case node_path() do | |
| nil -> [] | |
| path -> [{"NODE_PATH", path}] | |
| end | |
| {output, exit_code} = | |
| MuonTrap.cmd("node", [@runner_path, tmp_path], | |
| stderr_to_stdout: true, | |
| env: env, | |
| timeout: @timeout | |
| ) | |
| case exit_code do | |
| 0 -> | |
| :ok | |
| _ -> | |
| raise "Playwright script failed:\n#{output}" | |
| end | |
| after | |
| File.rm(tmp_path) | |
| end | |
| end | |
| defp maybe_prepend_login(script, %{userId: _}) do | |
| """ | |
| await page.goto('/dev/login_as/' + context.userId, { waitUntil: 'domcontentloaded' }); | |
| #{script} | |
| """ | |
| end | |
| defp maybe_prepend_login(script, _context), do: script | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment