# Playwright F# DSL — Agent Instructions
This document gives an agent everything needed to write F# Playwright tests using a computation-expression DSL and a type-safe page locator record.
---
## Step 0 — Confirm intent and target
Read the existing context before asking anything. Check whether the following are already clear:
1. **Intent** — does the user want to write end-to-end Playwright tests using this custom F# DSL?
2. **Target URL** — is there a URL (or a codebase with a startable server) to test against?
If intent is not clear from context, ask:
> *"Do you want to write F# Playwright tests using the custom computation-expression DSL against a web application?"*
If the URL is missing, ask:
> *"What is the URL of the application to test? (or share the codebase so I can start the server)"*
If intent is not clear abort.
---
## Step 1 — Check prerequisites
The Playwright CLI (`playwright-cli`) must be installed globally via npm or similar. Check with:
```bash
playwright-cli --version
```
If missing, ask if you are allowed to install it (`npm install -g @playwright/cli@latest`) or similar with `pnpm` or `yarn`. Ask user for guidance.
Use `playwright-cli --help` to familiarize yourself with the available commands.
---
## Step 2 — Set up the F# test project
Identify whether an F# test project exists or where a new test project should be created (ask if unclear).
### If an F# test project already exists
Add the Playwright NuGet package to it:
```bash
dotnet add package Microsoft.Playwright
```
### If no F# test project exists
Change working directory to the folder where the new test project should be created and run:
```bash
dotnet new console -lang F# -n Tests.E2E -o Tests.E2E
cd Tests.E2E
dotnet add package Microsoft.Playwright
dotnet add package Expecto
dotnet add package FsToolkit.ErrorHandling
dotnet add package Microsoft.NET.Test.Sdk
dotnet add package YoloDev.Expecto.TestSdk
```
Add the project to the solution if one exists, `using dotnet sln add Tests.E2E/Tests.E2E.fsproj`
### Download the DSL
Download `Dsl.fs`
```bash
curl -fsSLiH "Accept: text/plain" https://jannikbuschke.de/examples/playwright-dsl/definition.fs -o Tests.E2E/Dsl.fs
```
Register it first in `Tests.E2E.fsproj`:
```xml
```
---
## Step 3 — Explore the site
Use the Playwright CLI to inspect the target before writing any code. Use the playwright-cli, don't use any other method of exploring the site. If that doesn't work abort.
The accessible names you observe map directly to the `Locators` helpers used in the DSL (`Button "Sign in"`, `Label "Username"`, `Heading "Game lobby"`, etc.).
---
## Step 5 — Model the pages (PLR)
Create one record type per logical page or modal. Fields are either `Locator` (non-navigating) or `Transition<'next>` (navigating to another page).
```fsharp
open type Locators
type LoginPage = {
UsernameField: Locator // IPage -> ILocator
PasswordField: Locator
SigninButton: Transition // (IPage -> ILocator) * (unit -> HomePage)
}
and HomePage = {
CreateButton: Transition
ItemList: Locator
}
and CreateModal = {
NameField: Locator
SubmitButton: Transition
}
let rec login () : LoginPage = {
UsernameField = Label "Username"
PasswordField = Label "Password"
SigninButton = Button "Sign in", home
}
and home () : HomePage = {
CreateButton = Button "Create", createModal
ItemList = Locator "ul.items"
}
and createModal () : CreateModal = {
NameField = Label "Name"
SubmitButton = Button "Submit", home
}
let plr = {| Login = login; Home = home; CreateModal = createModal |}
```
**Rules:**
- `Locator = IPage -> ILocator` — every `Pw` helper returns this.
- `Transition<'t> = Locator * (unit -> 't)` — tuple of locator and thunk to next PLR.
- Use `let rec … and …` so page types can reference each other.
- For chained/filtered locators use `||>` (`Locator -> (ILocator -> ILocator) -> Locator`):
```fsharp
Locator "div.list"
||> _.Filter(LocatorFilterOptions(HasText = "Alice"))
||> _.GetByRole(AriaRole.Button)
||> _.First
```
---
## Step 6 — Write the tests
```fsharp
open Dsl
[]
let myTests =
testCaseTask "user can create an item" <| fun () ->
task {
use! pw = Playwright.CreateAsync()
use! browser = pw.Firefox.LaunchAsync()
use! ctx = browser.NewContextAsync()
let! page = ctx.NewPageAsync()
do! page.GotoAsync("here-goes-the-initial-url")
do!
pageTest (page, plr.Home()) {
click _.SigninLink // Transition — PLR becomes LoginPage
fill _.UsernameField "alice"
fill _.PasswordField "secret"
click _.SigninButton // Transition — PLR becomes HomePage
expectVisible _.CreateButton 5000.0f
click _.CreateButton // Transition — PLR becomes CreateModal
fill _.NameField "my item"
click _.SubmitButton // Transition — PLR becomes HomePage
waitFor _.ItemList 8000.0f
expectContainsText _.ItemList "my item"
}
}
```
Each `click` on a `Transition` advances the PLR automatically. The compiler prevents accessing fields that don't exist on the current page.