About playwright-release-notes
playwright-release-notes is a skill for contributors preparing Playwright release documentation. It guides creation of Node.js notes and Python, Java, and .NET translations, then conversion of each section into language-specific GitHub release markdown.
Key features
- 01Draft Node.js release notes
- 02Translate notes for Python, Java, and .NET
- 03Apply language-specific API names and examples
- 04Convert notes to GitHub release markdown
- 05Check documentation links
Use cases
- 01Prepare release notes across four language docs
- 02Adapt API examples for each language
- 03Create and validate release announcement markdown
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ microsoft/ playwright --skill playwright-release-notesAfter installing, type/playwright-release-notesin Claude Code to confirm it loaded.
Details
From the project READMERelease notes
In this skill, 1.xx is the release (for example 1.64).
Every release has a ## Version 1.xx section in four files in docs/src/:
| Language | Notes | Docs base URL | GitHub release in |
|---|---|---|---|
| Node.js | release-notes-js.md | https://playwright.dev/docs/ | microsoft/playwright |
| Python | release-notes-python.md | https://playwright.dev/python/docs/ | microsoft/playwright-python |
| Java | release-notes-java.md | https://playwright.dev/java/docs/ | microsoft/playwright-java |
| .NET | release-notes-csharp.md | https://playwright.dev/dotnet/docs/ | microsoft/playwright-dotnet |
Write the Node.js notes and the three translations in the same PR, so every language has its notes when it starts its release. The language repos then add only their own significant changes, and each one writes its GitHub release from its section (step 3).
1. Node.js notes
In docs/src/release-notes-js.md, add a ## Version 1.xx section above the previous version, and use previous versions as a guide:
- Highlights come first, each with an emoji heading and a short sample. Ask teammates for help with them.
- Then
### New APIswith####groups,### Breaking Changes ⚠️,### Announcementsand### Browser Versions. Print the browser versions withnode utils/print_versions.js. - Refer to APIs with docs link macros:
[`method: Page.frameLocator`],[`property: Page.webmcp`],[`event: Page.dialogClosed`],[`option: Tracing.start.ariaSnapshots`],[Locator]. Each language's docs show them with that language's names. - Link other docs pages with relative paths:
[test locks](./test-parallel.md#test-locks).
2. Python, Java and .NET translations
In each of release-notes-python.md, release-notes-java.md and release-notes-csharp.md, add a ## Version 1.xx section above the previous version. Translate it from the Node.js section. Compare the Version 1.63 sections of the files to see what past translations dropped and changed.
For every language:
- Keep the structure: highlights with emoji headings,
### New APIswith####groups,### Breaking Changes ⚠️,### Announcementsand### Browser Versions. - Remove what the language does not have:
- the test runner:
@playwright/test,test.*, config, fixtures, projects, reporters andnpx playwright testoptions - UI mode, the HTML report, component testing and the VS Code extension
- TypeScript-only features (generics, JSX,
tsconfig) anddevicesdescriptors - every API whose
langsexcludes the language (python,java,csharp). Checklangsindocs/src/api/; options shared between methods are defined indocs/src/api/params.md.
- the test runner:
- Use the language's API where it differs from Node.js:
- A language-specific option has its own
langs. For example,recordVideoFpsislangs: csharp, java, python, while Node.js hasrecordVideo.fps. alias-<lang>renames a member, e.g.alias-java: isVisibleforLocatorAssertions.toBeVisible.- The generated APIs in sibling checkouts confirm the names:
../playwright-python/playwright/sync_api/_generated.py,../playwright-java/playwright/src/main/java/com/microsoft/playwright/and../playwright-dotnet/src/Playwright/API/Generated/.
- A language-specific option has its own
- When a Node.js sample shows a library feature through
playwright.config.ts, show the API call instead. For example, the 1.64 video config became calls toScreencast.startandScreencast.showActions. - Keep API links as docs link macros, and link the language's own option where it differs (
[`option: Browser.newContext.recordVideoFps`]). In plain text, use the language's names. - A relative link must point to a page that exists in the language:
*-js.mdpages are Node.js only.npm run docreports broken links for each language. - Do not add changes that only a language port made. The language repo adds those to its notes.
Rewrite code samples in each language's style. The infra workflow checks the syntax of python, java and csharp snippets.
Python
- Use the sync API:
page.get_by_role("button").click(). Use snake_case names and Python values (True,False,None). - Options become keyword arguments:
page.get_by_role("button", name="Save"). Objects and arrays become dicts and lists:size={"width": 1920, "height": 1080}. - Aria roles and other string values stay strings:
"button","ai". - Events:
page.on("dialogclosed", handler). Assertions:expect(locator).to_have_text([...]). Print withprint(...). - Use the
alias-pythonnames:route.continue_(),locator.or_(),page.expect_request(). - Commands: write
playwright install, notnpx playwright install.
Java
| Node.js | Java |
|---|---|
await page.goto('...') | page.navigate("...");: no await, double quotes, semicolons, typed locals (Locator dialog = ...) |
page.getByRole('button', { name: 'Save' }) | page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Save")) |
options object { path: 'video.webm', fps: 60 } | options builder new Screencast.StartOptions().setPath(Paths.get("video.webm")).setFps(60) |
object literal { name: 'session-id', value: '42', url } | value class new Cookie("session-id", "42").setUrl(url) |
['a', 'b'], { a: 2 } | Arrays.asList("a", "b"), Map.of("a", 2); new String[] {"a", "b"} for assertion arguments |
property page.webmcp, context.tracing | getter page.webmcp(), context.tracing() |
event page.on('dialogclosed', handler) | page.onDialogClosed(handler) |
expect(locator).toHaveText(...) | assertThat(locator).hasText(...). Likewise toBeX → isX, toHaveX → hasX, toContainX → containsX, toMatchX → matchesX |
route.continue() and other members with alias-java | the alias, e.g. route.resume() |
npx playwright install --no-remove | mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --no-remove" |
"array", undefined, 'ai' in prose | "list", null, "ai" |
.NET
| Node.js | .NET |
|---|---|
await page.goto('...') | await page.GotoAsync("...");: PascalCase members, Async suffix on async methods, var locals, double quotes, semicolons |
page.getByRole('button', { name: 'Save' }) | page.GetByRole(AriaRole.Button, new() { Name = "Save" }) |
options object { path: 'video.webm', fps: 60 } | options initializer new() { Path = "video.webm", Fps = 60 } |
nested object size: { width: 1920, height: 1080 } | Size = new() { Width = 1920, Height = 1080 } |
object literal { name: 'session-id', value: '42', url } | new Cookie { Name = "session-id", Value = "42", Url = url } |
['a', 'b'], free-form object { a: 2 } | new[] { "a", "b" }, anonymous object new { a = 2 } |
string-literal union value 'ai', 'button' | enum member AriaSnapshotMode.Ai, AriaRole.Button |
property page.webmcp, context.tracing | property page.Webmcp, context.Tracing |
event page.on('dialogclosed', handler) | page.DialogClosed += (_, dialog) => { ... }; |
expect(locator).toHaveText(...) | await Expect(locator).ToHaveTextAsync(...); |
console.log(x) | Console.WriteLine(x); |
members with alias-csharp | the alias, e.g. page.request → page.APIRequest |
union-typed option, e.g. httpCredentials as a list | one property per type, e.g. HttpCredentialsList |
npx playwright install --no-remove | pwsh bin/Debug/netX/playwright.ps1 install --no-remove |
option names in prose (fontSize), undefined | FontSize, null |
Open one PR for all four files, titled docs: release notes for v1.xx. After it merges, cherry-pick it to release-1.xx with the playwright-cherry-pick skill.
3. GitHub release markdown
Each repo publishes the GitHub release for its language, with markdown converted from its ## Version 1.xx section on release-1.xx. Convert the notes yourself:
-
Leave out the
## Version 1.xxheading. Move every other heading up one level (###becomes##,####becomes###). -
Replace each link macro with a full link: the language's docs base URL from the table at the top, plus the path below. Use the language's name as the link text:
Macro Path after the docs base URL Node.js Python Java .NET [Locator]api/class-locatorLocatorLocatorLocatorLocator[`method: Page.frameLocator`]api/class-page#page-frame-locatorpage.frameLocator()page.frame_locator()page.frameLocator()Page.FrameLocator()[`method: Page.ariaSnapshot`]api/class-page#page-aria-snapshotpage.ariaSnapshot()page.aria_snapshot()page.ariaSnapshot()Page.AriaSnapshotAsync()[`method: Locator.first`]api/class-locator#locator-firstlocator.first()locator.firstlocator.first()Locator.First[`property: Page.webmcp`]api/class-page#page-webmcppage.webmcppage.webmcppage.webmcp()Page.Webmcp[`event: BrowserContext.dialogClosed`]api/class-browsercontext#browser-context-event-dialog-closedbrowserContext.on('dialogclosed')browser_context.on("dialogclosed")browserContext.onDialogClosed()BrowserContext.DialogClosed[`method: LocatorAssertions.toBeVisible`]api/class-locatorassertions#locator-assertions-to-be-visibleexpect(locator).toBeVisible()expect(locator).to_be_visible()assertThat(locator).isVisible()Expect(Locator).ToBeVisibleAsync()[`option: Page.content.includeShadow`]api/class-page#page-content-option-include-shadow`includeShadow``include_shadow``includeShadow``IncludeShadow`[Playwright MCP](./getting-started-mcp.md)getting-started-mcpPlaywright MCPPlaywright MCPPlaywright MCPPlaywright MCP -
An image
becomesin every language.
The page is class- and the class name in lower case. The anchor is the class and member names in kebab-case, so APIRequestContext.addCookies becomes api-request-context-add-cookies. Options add -option-<name>, and events add -event-<name>. Anchors use the names from docs/src/api/ even where a language renames the member, so Java's isVisible() links to #locator-assertions-to-be-visible.
The link text follows the language:
- Node.js: the class in lower camel case (
page,apiRequestContext,testConfig). Methods get(), properties none, events becomeon('<event in lower case>'), and assertions start withexpect(locator).,expect(page).orexpect(response).. - Python: the class and member in snake_case (
browser_context,api_request_context). Methods get(). A member is a property, without(), if_generated.pydeclares it with@property. Events becomeon("<event in lower case>"), and assertions start withexpect(locator).. - Java: the class in lower camel case and the
alias-javamember name. Methods and properties get(), events becomeon<Event>(), and assertions start withassertThat(locator).. - .NET: the class without the interface
I(Page,APIRequestContext) and the member exactly as the generatedI<Class>.csdeclares it. Async methods take theAsyncsuffix and(), sync methods get(), and properties and events get no parentheses. Assertions start withExpect(Locator)..
Check that every link resolves. playwright.dev already documents the release once the Node.js docs roll has landed. It serves minified HTML, so an anchor may appear without quotes (id=page-webmcp>):
grep -oE 'https://playwright\.dev/[^) ]+' <notes-file> | sort -u | while read -r url; do
page=${url%%#*}
html=$(curl -sfL "$page") || { echo "MISSING PAGE $url"; continue; }
if [[ "$url" == *"#"* ]]; then
anchor=${url#*#}
grep -qE "id=\"?${anchor}[\" >]" <<<"$html" || echo "MISSING ANCHOR $url"
fi
done
Then scan for anything that wasn't converted, and for stray spaces inside link text ([Page. DialogClosed], [`HttpCredentialsList `]). The first command applies to every language; then run the one for the language. They should print nothing:
# every language
grep -nE '\[`(method|property|event|option|param): |\]\(\./|\[[A-Z][A-Za-z]*\]([^(]|$)|\[[A-Za-z_]+\. |\[`[^`]+ `\]' <notes-file>
# Python: camelCase names in link text
grep -nE '\[[a-z_]+\.[a-z_]*[A-Z]|\[`[a-z]+[A-Z]' <notes-file>
# Java: JS-style events, assertions and properties
grep -nE "\.on\('|\b(toBe|toHave|toContain|toMatch)[A-Z]|\bawait\b|\barray\b|\bundefined\b|\[[a-z][A-Za-z]*\.[a-z][A-Za-z]*\]\(" <notes-file>
# .NET: lower-case classes or members, JS-style events
grep -nE "\.on\('|\bundefined\b|\[[a-z][A-Za-z]*\.|\[[A-Z][A-Za-z]*\.[a-z]|\[Expect\([a-z]" <notes-file>
If the conversion shows a problem in the notes themselves, such as an API the language does not have or a sample in the wrong style, fix the notes too (step 2). Otherwise the problem stays on playwright.dev.