Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 14 additions & 6 deletions .claude/skills/playwright-roll/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,31 @@ description: Roll Playwright .NET to a new version

Help the user roll to a new version of Playwright.
../../../ROLLING.md contains general instructions and scripts.
See also [CLAUDE.md](../../../CLAUDE.md) for build, test, and architecture details.

Start with running the roll script to update the version and regenerate the API to see the state of things.
Unless the exact version is specified by the user, you need to find the latest version of the driver.
Check the publish workflow here: https://github.com/microsoft/playwright/actions/workflows/publish_release.yml. The step that builds and publishes the driver contains the exact version you need.

Now, with the driver version known, always start with running the roll script to update the version and regenerate the API to see the state of things.

```bash
./build.sh --roll <driver-version>
```

Afterwards, work through the list of changes that need to be backported.
You can find a list of pull requests that might need to be taking into account in the issue titled "Backport changes".
Work through them one-by-one and check off the items that you have handled.
Not all of them will be relevant, some might have partially been reverted, etc. - so feel free to check with the upstream release branch.
You can find a list of pull requests that might need to be taking into account in the issue titled "Backport changes". Ignore the items that are already checked off.

Some items may be irrelevant to the .NET implementation - feel free to check with the upstream.

Some items may be connected, for example when the API has changed multiple times. In this case, handle them alltogether, aligning with the latest change. Check upstream to see the latest implementation.

Otherwise, work through items one-by-one.

Rolling includes:
- updating client implementation to match changes in the upstream JS implementation (see ../playwright/packages/playwright-core/src/client)
- adding a couple of new tests to verify new/changed functionality

## Tips & Tricks
- Project checkouts are in the parent directory (`../`).
- when updating checkboxes, store the issue content into /tmp and edit it there, then update the issue based on the file
- use the "gh" cli to interact with GitHub
- When updating checkboxes, store the issue content into /tmp and edit it there, then update the issue based on the file.
- Use the "gh" cli to interact with GitHub.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
fail-fast: false
matrix:
browser: [chromium, firefox, webkit]
os: [windows-latest, ubuntu-latest, macos-latest]
os: [windows-latest, ubuntu-latest, macos-15-large]
steps:
- uses: actions/checkout@v5
- name: Setup .NET Core
Expand Down
50 changes: 50 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Playwright .NET

## Building

```bash
./build.sh --download-driver # download the Playwright driver
dotnet build ./src # build the entire solution
```

## Running tests

Follow `.github/workflows/tests.yml` for the canonical sequence.

```bash
# Install browsers (pick one: chromium, firefox, webkit)
pwsh src/Playwright/bin/Debug/netstandard2.0/playwright.ps1 install --with-deps chromium

# Run tests
BROWSER=chromium dotnet test ./src/Playwright.Tests/Playwright.Tests.csproj -c Debug -f net8.0 --logger:"console;verbosity=detailed"
```

Tests take ~8 minutes. Always save output to a file and grep from there:

```bash
BROWSER=chromium dotnet test ./src/Playwright.Tests/Playwright.Tests.csproj \
-c Debug -f net8.0 --logger:"console;verbosity=detailed" > /tmp/test-results.txt 2>&1
grep "^ Failed" /tmp/test-results.txt # list failures
tail -5 /tmp/test-results.txt # summary
```

## Architecture

### Generated vs hand-written code
- Public API interfaces (e.g. `src/Playwright/API/Generated/IPage.cs`) are **generated** by `../playwright/utils/doclint/generateDotnetApi.js` from the upstream API docs. Do not hand-edit these — update the generator instead.
- The generator uses `classNameMap` for type mappings (e.g. `Disposable` → `IAsyncDisposable`, `boolean` → `bool`). Add entries there when a Playwright type should map to a different .NET type.
- The generator skips generating interface files for types like `TimeoutException` and `IAsyncDisposable` that map to built-in .NET types.
- Supplement interfaces (`src/Playwright/API/Supplements/`) are hand-written and extend the generated interfaces with .NET-specific overloads.
- Internal implementations live in `src/Playwright/Core/` (namespace `Microsoft.Playwright.Core`). These implement both the generated and supplement interfaces.

### Key patterns
- All Playwright objects extend `ChannelOwner` and communicate via `SendMessageToServerAsync`.
- `Connection.cs` has a factory switch that creates the right `ChannelOwner` subclass based on `ChannelOwnerType`.
- New channel object types require: enum entry in `ChannelOwnerType.cs`, case in `Connection.cs`, initializer in `Transport/Protocol/Generated/`, and a `Core/` class.
- Public APIs should use .NET standard types (e.g. `IAsyncDisposable`) not custom Playwright types. Internal helpers (e.g. `Disposable` class in `Core/`) stay internal.

## Commits
- Do not include "co-authored" block in the commit message.

## Rolling to a new Playwright version
See [.claude/skills/playwright-roll/SKILL.md](.claude/skills/playwright-roll/SKILL.md).
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->145.0.7632.6<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Chromium <!-- GEN:chromium-version -->146.0.7680.31<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| WebKit <!-- GEN:webkit-version -->26.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->146.0.1<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->148.0.2<!-- GEN:stop --> | ✅ | ✅ | ✅ |

Playwright for .NET is the official language port of [Playwright](https://playwright.dev), the library to automate [Chromium](https://www.chromium.org/Home), [Firefox](https://www.mozilla.org/en-US/firefox/new/) and [WebKit](https://webkit.org/) with a single API. Playwright is built to enable cross-browser web automation that is **ever-green**, **capable**, **reliable** and **fast**.

Expand Down
2 changes: 1 addition & 1 deletion src/Common/Version.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<AssemblyVersion>1.58.0</AssemblyVersion>
<PackageVersion>$(AssemblyVersion)</PackageVersion>
<DriverVersion>1.58.1</DriverVersion>
<DriverVersion>1.59.0-alpha-2026-03-24</DriverVersion>
<ReleaseVersion>$(AssemblyVersion)</ReleaseVersion>
<FileVersion>$(AssemblyVersion)</FileVersion>
<NoDefaultExcludes>true</NoDefaultExcludes>
Expand Down
46 changes: 23 additions & 23 deletions src/Playwright.TestingHarnessTest/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/Playwright.TestingHarnessTest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "playwright.testingharnesstest",
"private": true,
"devDependencies": {
"@playwright/test": "1.58.1",
"@playwright/test": "1.59.0-alpha-2026-03-24",
"@types/node": "^22.12.0",
"fast-xml-parser": "^4.5.0"
}
Expand Down
23 changes: 23 additions & 0 deletions src/Playwright.Tests/BrowserContextStorageStateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,27 @@ public async Task ShouldSerializeStorageStateWithLoneSurrogates()
// It should get replaced by the utf8 replacement char (U+FFFD)
StringAssert.Contains(@"""value"":""\uFFFD""", storageState);
}

[PlaywrightTest("browsercontext-storage-state.spec.ts", "should set local storage via setStorageState")]
public async Task ShouldSetLocalStorageViaSetStorageState()
{
await using var context = await Browser.NewContextAsync();
var page = await context.NewPageAsync();
await page.RouteAsync("**/*", (route) =>
{
route.FulfillAsync(new() { Body = "<html></html>" });
});
await page.GotoAsync("https://www.example.com");
var localStorage = await page.EvaluateAsync<string>("window.localStorage.getItem('name1')");
Assert.IsNull(localStorage);

using var tempDir = new TempDirectory();
string path = Path.Combine(tempDir.Path, "storage-state.json");
File.WriteAllText(path, @"{""cookies"":[],""origins"":[{""origin"":""https://www.example.com"",""localStorage"":[{""name"":""name1"",""value"":""value1""}]}]}");
await context.SetStorageStateAsync(path);

await page.GotoAsync("https://www.example.com");
localStorage = await page.EvaluateAsync<string>("window.localStorage.getItem('name1')");
Assert.AreEqual("value1", localStorage);
}
}
18 changes: 10 additions & 8 deletions src/Playwright.Tests/BrowserContextTimezoneIdTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,40 +29,42 @@ public class BrowserContextTimezoneIdTests : BrowserTestEx
[PlaywrightTest("browsercontext-timezone-id.spec.ts", "should work")]
public async Task ShouldWork()
{
// Note: timezone names are rendered differently between browsers and platforms,
// so we only check the GMT offset.
await using var browser = await Playwright[TestConstants.BrowserName].LaunchAsync();

const string func = "() => new Date(1479579154987).toString()";
await using (var context = await browser.NewContextAsync(new() { TimezoneId = "America/Jamaica", Locale = "en-US" }))
{
var page = await context.NewPageAsync();
string result = await page.EvaluateAsync<string>(func);
Assert.AreEqual(
"Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)",
StringAssert.Contains(
"Sat Nov 19 2016 13:12:34 GMT-0500",
result);
}

await using (var context = await browser.NewContextAsync(new() { TimezoneId = "Pacific/Honolulu", Locale = "en-US" }))
{
var page = await context.NewPageAsync();
Assert.AreEqual(
"Sat Nov 19 2016 08:12:34 GMT-1000 (Hawaii-Aleutian Standard Time)",
StringAssert.Contains(
"Sat Nov 19 2016 08:12:34 GMT-1000",
await page.EvaluateAsync<string>(func));
}

var buenosAires = BrowserName == "firefox" ? "America/Argentina/Buenos_Aires" : "America/Buenos_Aires";
await using (var context = await browser.NewContextAsync(new() { TimezoneId = buenosAires, Locale = "en-US" }))
{
var page = await context.NewPageAsync();
Assert.AreEqual(
"Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)",
StringAssert.Contains(
"Sat Nov 19 2016 15:12:34 GMT-0300",
await page.EvaluateAsync<string>(func));
}

await using (var context = await browser.NewContextAsync(new() { TimezoneId = "Europe/Berlin", Locale = "en-US" }))
{
var page = await context.NewPageAsync();
Assert.AreEqual(
"Sat Nov 19 2016 19:12:34 GMT+0100 (Central European Standard Time)",
StringAssert.Contains(
"Sat Nov 19 2016 19:12:34 GMT+0100",
await page.EvaluateAsync<string>(func));
}
}
Expand Down
3 changes: 1 addition & 2 deletions src/Playwright.Tests/BrowserContextViewportMobileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,7 @@ public async Task ShouldDetectTouchWhenApplyingViewportWithTouches()

var page = await context.NewPageAsync();
await page.GotoAsync(Server.EmptyPage);
await page.AddScriptTagAsync(new() { Url = Server.Prefix + "/modernizr.js" });
Assert.True(await page.EvaluateAsync<bool>("() => Modernizr.touchevents"));
Assert.True(await page.EvaluateAsync<bool>("() => 'ontouchstart' in window || !!window.TouchEvent"));
}

[PlaywrightTest("browsercontext-viewport-mobile.spec.ts", "should support landscape emulation")]
Expand Down
27 changes: 27 additions & 0 deletions src/Playwright.Tests/PageAddInitScriptTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,31 @@ public async Task ShouldWorkAfterACrossOriginNavigation()
await Page.GotoAsync(Server.Prefix + "/tamperable.html");
Assert.AreEqual(123, await Page.EvaluateAsync<int>("() => window.result"));
}

[PlaywrightTest("page-add-init-script.spec.ts", "should remove init script after dispose")]
public async Task ShouldRemoveInitScriptAfterDispose()
{
var disposable = await Page.AddInitScriptAsync("window.injected = 123;");
await Page.GotoAsync(Server.Prefix + "/tamperable.html");
Assert.AreEqual(123, await Page.EvaluateAsync<int>("() => window.result"));

await disposable.DisposeAsync();
await Page.GotoAsync(Server.Prefix + "/tamperable.html");
Assert.IsNull(await Page.EvaluateAsync("() => window.injected"));
}

[PlaywrightTest("page-add-init-script.spec.ts", "should remove one of multiple init scripts after dispose")]
public async Task ShouldRemoveOneOfMultipleInitScriptsAfterDispose()
{
var disposable1 = await Page.AddInitScriptAsync("window.script1 = 1;");
await Page.AddInitScriptAsync("window.script2 = 2;");
await Page.GotoAsync(Server.Prefix + "/tamperable.html");
Assert.AreEqual(1, await Page.EvaluateAsync<int>("() => window.script1"));
Assert.AreEqual(2, await Page.EvaluateAsync<int>("() => window.script2"));

await disposable1.DisposeAsync();
await Page.GotoAsync(Server.Prefix + "/tamperable.html");
Assert.IsNull(await Page.EvaluateAsync("() => window.script1"));
Assert.AreEqual(2, await Page.EvaluateAsync<int>("() => window.script2"));
}
}
Loading
Loading