diff --git a/.claude/skills/playwright-roll/SKILL.md b/.claude/skills/playwright-roll/SKILL.md index f998352ad..39e8e737e 100644 --- a/.claude/skills/playwright-roll/SKILL.md +++ b/.claude/skills/playwright-roll/SKILL.md @@ -16,34 +16,91 @@ Now, with the driver version known, always start with running the roll script to ./build.sh --roll ``` -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". Ignore the items that are already checked off. +Afterwards, walk through the upstream changes that affect the Java client and port the relevant ones. -Some items may be irrelevant to the .NET implementation - feel free to check with the upstream. +## Determining what to port -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. +List the upstream commits that touched a client-relevant path since the last release. The paths cover everything that can change the public Java surface or the wire protocol: -Otherwise, work through items one-by-one. +- `docs/src/api/` — the source of truth for `api.json`. Method/option additions, removals, and `langs:` filter changes flow from here. +- `packages/playwright-core/src/client/` — the JS client implementation that the Java client mirrors. +- `packages/isomorphic/` — selector engines, locator generation/parsing, and aria-snapshot logic shared between client and server. Changes here can affect client-side helpers like `getByRoleSelector`. +- `packages/playwright/src/matchers/matchers.ts` — assertion-method definitions. Changes here usually correspond to new options on `LocatorAssertions` / `PageAssertions`. +- `packages/protocol/src/protocol.yml` and `packages/protocol/spec/**.yml` — the wire protocol schema. Method/event additions, parameter renames, and result-shape changes affect what the .NET implementation classes need to send/receive. -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 +```bash +cd ~/playwright +PREV_TAG=$(git tag | grep -E '^v1\.[0-9]+\.[0-9]+$' | sort -V | tail -1) # e.g. v1.59.1 +git log "$PREV_TAG"..HEAD --oneline -- \ + 'docs/src/api/' \ + 'packages/playwright-core/src/client/' \ + 'packages/isomorphic/' \ + 'packages/playwright/src/matchers/matchers.ts' \ + 'packages/protocol/src/protocol.yml' \ + 'packages/protocol/spec/' +``` + +Walk that list top-to-bottom (oldest-first is easier — newest is at top, so reverse). For each commit: +1. Read the commit (`git show `) to see what client/protocol/docs changed. +2. If it's JS-internal (bundling, dispatcher conventions, electron, mcp, dashboard, trace-viewer, test-runner) — skip. +3. If it touches `docs/src/api/` or types, check `langs:` annotations — features marked with languages other than "csharp" do not apply. +4. If it adds/changes a public API method or option that applies to .NET, port it. The 'build.sh --roll' already regenerated the types/options, so we usually only need to update the implementation. +5. Watch for follow-up reverts — a "feat: X" commit might be undone by a later "Revert X". Check whether the change still exists in HEAD before porting. +6. Maintain a running notes file (e.g. `/tmp/roll-notes.md`) listing each upstream PR as ported / skipped / verified-already-supported, with a one-line reason. This file becomes the body of the eventual PR. +7. Do not forget to port tests that cover .NET-relevant changes. At least one test per every API method/option to excercise the new code paths. Some server-side implementation tests are not needed in this port. + +## Mimicking the JavaScript implementation + +The .NET client is a port of the JS client in `../playwright/packages/playwright-core/src/client/`. When implementing a new or changed method, always read the corresponding JS file first and mirror its logic: + +``` +../playwright/packages/playwright-core/src/client/browserContext.ts +../playwright/packages/playwright-core/src/client/page.ts +... +``` + +**Read the final state at the release tag, not just the introducing PR diff.** When a feature evolves across multiple PRs (e.g. a method was introduced in one PR, then some options were added in follow-ups, then some logic was changed), the original PR's diff is a misleading reference — the final shape lives in `git show v1.X.0:packages/playwright-core/src/client/foo.ts`. Always read the v1.X.0 file before porting non-trivial logic. + +Key translation rules: + +**Protocol calls** — `await this._channel.methodName(params)` → `SendMessageToServerAsync("methodName", paramsDictionary)` + +**Extracting a returned channel object from a result** — JS uses `SomeClass.from(result.foo)` which resolves the JS-side object for a channel reference. In .NET, extract it from the connection: `.GetObject("foo", _connection)` + +**Watch for channel migrations.** A method that lived on `BrowserContext` may move to `Tracing` (and vice-versa) without notice — the protocol spec is the source of truth. If a `SendMessageToServerAsync("foo", ...)` call suddenly fails, grep `packages/protocol/spec/*.yml` to find which channel actually owns `foo` now. + +**Wire format changes can be subtle.** A method's result shape can change without renaming anything. These are easy to miss when walking client.ts because the JS code just adapts; the regression only surfaces when an existing test's deserialization breaks. Always diff `packages/protocol/spec/*.yml` for each commit on the walk list, not just the client code. + +**Update tests similarly to upstream.** When upstream removes or modifies a test, apply similar changes. ## Renaming generated types When the API generator produces an unhelpful name for a return type (e.g. `Bind` instead of `BrowserBindResult`), you can control it by adding a struct alias in the upstream docs. -In the docs markdown file (e.g. `docs/src/api/class-browser.md`), change the return type from `<[Object]>` to `<[Object=DesiredName]>`: +In the docs markdown file (e.g. `docs/src/api/class-browser.md`), add `alias` (applies to all languages) or `alias-csharp` (overrides for .NET only) to the `<[Object]>` type: ```diff -- returns: <[Object]> -+- returns: <[Object=BrowserBindResult]> - - `endpoint` <[string]> ++- returns: <[Object]> ++ - alias-csharp: BrowserBindResult +``` + +Use `alias-csharp` (in addition to bare `alias`) when upstream already declared a different `alias` for other languages and you only want to override .NET's name — typically to preserve a name we've already shipped and don't want to break. After making this change, re-run `./build.sh --roll ` to regenerate, then update any hand-written implementation code to use the new type name. The upstream docs change must also land in `microsoft/playwright` via a separate PR or it will revert on the next roll. + +## Running the full test suite + +After porting, run the chromium suite end-to-end before opening the PR — many regressions only surface in older tests that depended on now-changed behavior. Tests take ~8–10 minutes: + +```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 ``` -The `=Name` syntax sets `structName` on the parsed type, which the .NET generator uses directly as the class name. After making this change, re-run `./build.sh --roll ` to regenerate, then update any hand-written implementation code to use the new type name. +Run as a background task — don't block the conversation on it. For each failure: check whether it's a flake (port collision, browser timing) by running the single test in isolation, then investigate whether your roll introduced it or it's pre-existing. ## 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 running git commands against an upstream repo, always use `git -C /path/to/repo ` instead of `cd /path/to/repo && git `. diff --git a/README.md b/README.md index d6dccdd58..6f02ca2d5 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 147.0.7727.15 | ✅ | ✅ | ✅ | +| Chromium 148.0.7778.96 | ✅ | ✅ | ✅ | | WebKit 26.4 | ✅ | ✅ | ✅ | -| Firefox 148.0.2 | ✅ | ✅ | ✅ | +| Firefox 150.0.2 | ✅ | ✅ | ✅ | 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**. diff --git a/src/Common/Version.props b/src/Common/Version.props index 5680c52f3..74d3491c4 100644 --- a/src/Common/Version.props +++ b/src/Common/Version.props @@ -2,7 +2,7 @@ 1.59.0 $(AssemblyVersion) - 1.59.1 + 1.60.0 $(AssemblyVersion) $(AssemblyVersion) true diff --git a/src/Playwright.TestingHarnessTest/package-lock.json b/src/Playwright.TestingHarnessTest/package-lock.json index aff0d0ac5..e4082565f 100644 --- a/src/Playwright.TestingHarnessTest/package-lock.json +++ b/src/Playwright.TestingHarnessTest/package-lock.json @@ -7,19 +7,19 @@ "": { "name": "playwright.testingharnesstest", "devDependencies": { - "@playwright/test": "1.59.1", + "@playwright/test": "1.60.0", "@types/node": "^22.12.0", "fast-xml-parser": "^4.5.0" } }, "node_modules/@playwright/test": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", - "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.59.1" + "playwright": "1.60.0" }, "bin": { "playwright": "cli.js" @@ -77,13 +77,13 @@ } }, "node_modules/playwright": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", - "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.59.1" + "playwright-core": "1.60.0" }, "bin": { "playwright": "cli.js" @@ -96,9 +96,9 @@ } }, "node_modules/playwright-core": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", - "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -124,12 +124,12 @@ }, "dependencies": { "@playwright/test": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", - "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", "dev": true, "requires": { - "playwright": "1.59.1" + "playwright": "1.60.0" } }, "@types/node": { @@ -158,19 +158,19 @@ "optional": true }, "playwright": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", - "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", "dev": true, "requires": { "fsevents": "2.3.2", - "playwright-core": "1.59.1" + "playwright-core": "1.60.0" } }, "playwright-core": { - "version": "1.59.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", - "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", "dev": true }, "strnum": { diff --git a/src/Playwright.TestingHarnessTest/package.json b/src/Playwright.TestingHarnessTest/package.json index 5584de4be..a72015e8e 100644 --- a/src/Playwright.TestingHarnessTest/package.json +++ b/src/Playwright.TestingHarnessTest/package.json @@ -2,7 +2,7 @@ "name": "playwright.testingharnesstest", "private": true, "devDependencies": { - "@playwright/test": "1.59.1", + "@playwright/test": "1.60.0", "@types/node": "^22.12.0", "fast-xml-parser": "^4.5.0" } diff --git a/src/Playwright.TestingHarnessTest/tests/baseTest.ts b/src/Playwright.TestingHarnessTest/tests/baseTest.ts index 55e850795..30727841f 100644 --- a/src/Playwright.TestingHarnessTest/tests/baseTest.ts +++ b/src/Playwright.TestingHarnessTest/tests/baseTest.ts @@ -40,7 +40,7 @@ export const test = base.extend<{ launchServer: async ({ playwright }, use) => { const servers: BrowserServer[] = []; await use(async ({port}: {port: number}) => { - servers.push(await playwright.chromium.launchServer({ port })); + servers.push(await playwright.chromium.launchServer({ host: '127.0.0.1', port })); }); for (const server of servers) await server.close(); diff --git a/src/Playwright.Tests/Assertions/PageAssertionsTests.cs b/src/Playwright.Tests/Assertions/PageAssertionsTests.cs index 14b0497d1..c32ce5218 100644 --- a/src/Playwright.Tests/Assertions/PageAssertionsTests.cs +++ b/src/Playwright.Tests/Assertions/PageAssertionsTests.cs @@ -90,4 +90,28 @@ public async Task ShouldSupportToHaveURLAsync() await Expect(Page).ToHaveURLAsync("DATA:teXT/HTml,
a
", new() { IgnoreCase = true }); await Expect(Page).ToHaveURLAsync(new Regex("DATA:teXT/HTml,
a
"), new() { IgnoreCase = true }); } + + [PlaywrightTest("playwright-test/playwright.expect.misc.spec.ts", "should support toMatchAriaSnapshot on page")] + public async Task ShouldSupportToMatchAriaSnapshotOnPage() + { + await Page.SetContentAsync("

title

"); + await Expect(Page).ToMatchAriaSnapshotAsync(@" + - heading ""title"" + - button ""click me"" + "); + + var exception = await PlaywrightAssert.ThrowsAsync(() => Expect(Page).ToMatchAriaSnapshotAsync(@" + - heading ""missing"" + ", new() { Timeout = 100 })); + StringAssert.Contains("Page expected to match Aria snapshot", exception.Message); + } + + [PlaywrightTest("playwright-test/playwright.expect.misc.spec.ts", "should include aria snapshot in failure message")] + public async Task ShouldIncludeAriaSnapshotInFailureMessage() + { + await Page.SetContentAsync("

actual title

"); + var exception = await PlaywrightAssert.ThrowsAsync(() => Expect(Page.Locator("h1")).ToHaveTextAsync("wrong", new() { Timeout = 100 })); + StringAssert.Contains("Aria snapshot", exception.Message); + StringAssert.Contains("heading", exception.Message); + } } diff --git a/src/Playwright.Tests/BrowserContextEventsTests.cs b/src/Playwright.Tests/BrowserContextEventsTests.cs index 87e4f4409..6dca3da48 100644 --- a/src/Playwright.Tests/BrowserContextEventsTests.cs +++ b/src/Playwright.Tests/BrowserContextEventsTests.cs @@ -216,4 +216,62 @@ public async Task WebErrorEventShouldWork() Assert.AreEqual(Page, webError.Page); StringAssert.Contains("boom", webError.Error); } + + [PlaywrightTest("browsercontext-events.spec.ts", "weberror event should include location")] + public async Task WebErrorEventShouldIncludeLocation() + { + var tsc = new TaskCompletionSource(); + Context.WebError += (sender, e) => tsc.TrySetResult(e); + await Page.SetContentAsync(@""); + var webError = await tsc.Task; + Assert.IsNotNull(webError.Location); + StringAssert.Contains("boom", webError.Error); + } + + [PlaywrightTest("browsercontext-events.spec.ts", "should fire pageLoad")] + public async Task ShouldFirePageLoad() + { + var tcs = new TaskCompletionSource(); + Context.PageLoad += (_, p) => tcs.TrySetResult(p); + var page = await Context.NewPageAsync(); + await page.GotoAsync(Server.EmptyPage); + var loaded = await tcs.Task; + Assert.AreSame(page, loaded); + } + + [PlaywrightTest("browsercontext-events.spec.ts", "should fire pageClose")] + public async Task ShouldFirePageClose() + { + var tcs = new TaskCompletionSource(); + Context.PageClose += (_, p) => tcs.TrySetResult(p); + var page = await Context.NewPageAsync(); + await page.CloseAsync(); + var closed = await tcs.Task; + Assert.AreSame(page, closed); + } + + [PlaywrightTest("browsercontext-events.spec.ts", "should fire frameAttached and frameNavigated")] + public async Task ShouldFireFrameAttachedAndFrameNavigated() + { + var attachedTcs = new TaskCompletionSource