playwright-java / test
AGENTS.md1.6k starsChanged 25 days agoApache-2.0View on GitHub
These rules say how to write playwright/src/test/java/com/microsoft/playwright/tests/{page,library} from tests/page and tests/library in microsoft/playwright (the checkout in ../playwright; the commit the port is based on is in upstream_base.txt, together with the commands that list the upstream changes still to port). Use them for every new or changed upstream test. The port is line by line, with zero deviations. A reviewer reads the .spec.ts file and the .java file side by side. Each upstream statement maps to one Java statement, or to…
Adopted by
Steady: 0 new projects took it in the last 4 weeks, none the 4 weeks before.thin data
Who's talking about microsoft/playwright-java
- microsoft/playwright“[Feature] Support for Raspberry PI”GitHub · Jan 12, 2021 · 34 reactions
- microsoft/playwright“[Feature] Driver - allow to install only selected browsers”GitHub · Sep 13, 2020 · 14 reactions
- Stack Overflow“Executing Puppeteer using Java”Stack Overflow · Nov 27, 2020 · 10 votes
- microsoft/playwright“[Feature]: Sign windows Binaries and browser exe's”GitHub · Mar 12, 2025 · 4 reactions
- Stack Overflow“Unable to download with playwright headless true from url link”Stack Overflow · Mar 30, 2023 · 4 votes
The biggest 5 of 56 mentions linking to the repository that our crawl found. Each line is their own headline.
See all 56
Talked about on GitHub and Stack Overflow (56 times).
On GitHub · 50
- Jan 12, 2021[Feature] Support for Raspberry PI — microsoft/playwright34 reactions · 8 comments
- Sep 13, 2020[Feature] Driver - allow to install only selected browsers — microsoft/playwright14 reactions · 3 comments
- Mar 12, 2025[Feature]: Sign windows Binaries and browser exe's — microsoft/playwright4 reactions · 8 comments
- Apr 2, 2023explore adding first-class support for playwright — karatelabs/karate3 reactions · 31 comments
- Jul 15, 2026[Feature]: Support order-insensitive array matching in toHaveText — microsoft/playwright2 reactions · 2 comments
45 more on GitHub
- Jan 20, 2025Attempt 1 failed: BrowserType.launch: ENOSPC: no space left on device, mkdtemp '/tmp/playwright-artifacts-aNE1l9' — ScrapeGraphAI/Scrapegraph-ai1 reaction · 2 comments
- Sep 30, 2023JUnit codegen — microsoft/playwright1 reaction · 7 comments
- May 24, 2023Updated documentation URLs to use Playwright Homepage — microsoft/playwright1 reaction
- May 18, 2021fix: compute payload mime type on server — microsoft/playwright1 reaction
- Nov 7, 2026chore(deps): bump com.microsoft.playwright:playwright from 1.63.0 to 1.64.0 — anandbagmar/teswiz0 reactions
- Nov 6, 2026chore: bump com.microsoft.playwright:playwright from 1.62.0 to 1.64.0 — pmodv/WebGoat0 reactions
- Nov 5, 2026chore: bump com.microsoft.playwright:playwright from 1.62.0 to 1.64.0 — JoFloresGutierrez06/Actividad-12-ciber0 reactions · 1 comment
- Nov 5, 2026chore: bump com.microsoft.playwright:playwright from 1.62.0 to 1.64.0 — Jcmalanco/Actividad-12-ciber0 reactions
- Nov 3, 2026Bump com.microsoft.playwright:playwright from 1.58.0 to 1.64.0 in /backend — RakshitRao2712/Heimdall0 reactions
- Nov 2, 2026chore: bump com.microsoft.playwright:playwright from 1.62.0 to 1.64.0 — WebGoat/WebGoat0 reactions
- Nov 2, 2026Bump com.microsoft.playwright:playwright from 1.60.0 to 1.64.0 — Aldaviva/playwright-extra-install-arguments0 reactions
- Nov 2, 2026build(deps-dev): bump com.microsoft.playwright:playwright from 1.62.0 to 1.64.0 — lordlamer/jknowledgeroot0 reactions · 1 comment
- Nov 1, 2026chore(deps): bump com.microsoft.playwright:playwright from 1.62.0 to 1.64.0 — bohdankordon/vulcan-schedule-monitor0 reactions
- Nov 1, 2026Bump com.microsoft.playwright:playwright from 1.61.0 to 1.64.0 — earlbertmercado/playwright-saucedemo-java0 reactions
- Oct 31, 2026build: bump com.microsoft.playwright:playwright from 1.63.0 to 1.64.0 — samueleniccolai-dev/gerio-lab0 reactions
- Oct 31, 2026chore(deps-dev): bump com.microsoft.playwright:playwright from 1.63.0 to 1.64.0 — RsheedAlo/medtech-qa-showcase0 reactions
- Oct 31, 2026chore(deps-dev): bump com.microsoft.playwright:playwright from 1.63.0 to 1.64.0 in /api — araujovito/atlas-cientifico0 reactions
- Oct 30, 2026Bump com.microsoft.playwright:playwright from 1.63.0 to 1.64.0 in /java-app — gj-boltron/github-actions-demo0 reactions
- Oct 30, 2026chore(deps): bump com.microsoft.playwright:playwright from 1.63.0 to 1.64.0 — kestra-io/plugin-playwright0 reactions · 3 comments
- Oct 30, 2026chore(deps): bump com.microsoft.playwright:playwright from 1.62.0 to 1.64.0 — iida-27z/java-env-tmp0 reactions · 1 comment
- Oct 30, 2026Bump com.microsoft.playwright:playwright from 1.62.0 to 1.64.0 — yeshallbeasgods/refined-joyous-camellia0 reactions
- Oct 29, 2026chore(deps): bump com.microsoft.playwright:playwright from 1.62.0 to 1.64.0 — thiyagarajan2002/ai-testing-agent-v30 reactions
- Oct 28, 2026chore: bump com.microsoft.playwright:playwright from 1.62.0 to 1.64.0 — Hajarelb/workshop3-G20 reactions
- Oct 28, 2026chore(deps): bump com.microsoft.playwright:playwright from 1.47.0 to 1.64.0 — huawei-zxs/kestra0 reactions · 1 comment
- Oct 28, 2026chore: bump com.microsoft.playwright:playwright from 1.62.0 to 1.64.0 — darrylsarkisian-debug/acme-webgoat0 reactions
- Oct 28, 2026build(deps): bump com.microsoft.playwright:playwright from 1.61.0 to 1.64.0 — XSU15/kestra0 reactions · 1 comment
- Oct 27, 2026build(deps): bump com.microsoft.playwright:playwright from 1.61.0 to 1.64.0 — AiSecurityServices/kestra0 reactions · 1 comment
- Oct 27, 2026chore: bump com.microsoft.playwright:playwright from 1.62.0 to 1.64.0 — sbshivam10/WebGoatFresh0 reactions
- Oct 27, 2026chore(deps): bump com.microsoft.playwright:playwright from 1.63.0 to 1.64.0 — Mariyo/ktor-template0 reactions
- Oct 27, 2026chore(deps): Bump com.microsoft.playwright:playwright from 1.62.0 to 1.64.0 — conciso/keycloak-extensions0 reactions · 1 comment
- Oct 26, 2026Bump com.microsoft.playwright:playwright from 1.62.0 to 1.64.0 — silvaemy/WebGoat-Testing0 reactions
- Oct 26, 2026Bump com.microsoft.playwright:playwright from 1.63.0 to 1.64.0 — quarkus-qe/quarkus-startstop0 reactions
- Oct 26, 2026Bump com.microsoft.playwright:playwright from 1.62.0 to 1.64.0 — patrickf314/ui-test-components0 reactions
- Oct 26, 2026Bump com.microsoft.playwright:playwright from 1.63.0 to 1.64.0 in /quarkus-workshop-super-heroes — quarkusio/quarkus-workshops0 reactions · 1 comment
- Oct 26, 2026chore(deps): bump com.microsoft.playwright:playwright from 1.62.0 to 1.64.0 — RafeArnold/the-mind0 reactions
- Oct 26, 2026Bump com.microsoft.playwright:playwright from 1.55.0 to 1.64.0 — JesseEmerson7/issue-tracker0 reactions
- Oct 26, 2026Bump com.microsoft.playwright:playwright from 1.63.0 to 1.64.0 — kihdev/playwright-stealth-4j0 reactions
- Oct 26, 2026Bump com.microsoft.playwright:playwright from 1.63.0 to 1.64.0 — martinelli-workshops/sdd-lab0 reactions
- Oct 26, 2026Bump com.microsoft.playwright:playwright from 1.63.0 to 1.64.0 in /playwright/java — bonigarcia/browser-automation-apis0 reactions
- Oct 26, 2026chore(deps-dev): bump com.microsoft.playwright:playwright from 1.63.0 to 1.64.0 in /backend — Everything-is-Code/migration-toolkit-rhcl0 reactions
- Oct 26, 2026Bump com.microsoft.playwright:playwright from 1.62.0 to 1.64.0 — kompars/webpush0 reactions
- Oct 26, 2026Bump com.microsoft.playwright:playwright from 1.63.0 to 1.64.0 — simasch/hrs0 reactions
- Oct 26, 2026Bump com.microsoft.playwright:playwright from 1.63.0 to 1.64.0 — martinelli-workshops/alpenfeld-portal0 reactions
- Oct 26, 2026Bump com.microsoft.playwright:playwright from 1.63.0 to 1.64.0 in the playwright group across 1 directory — StitchMl/authenticated-media-stream-analyzer0 reactions · 1 comment
- Oct 25, 2026Bump com.microsoft.playwright:playwright from 1.61.0 to 1.64.0 in /packages/java — virtualfanc/slybrowser0 reactions
On Stack Overflow · 6
- Nov 27, 2020Executing Puppeteer using Java10 votes
- Mar 30, 2023Unable to download with playwright headless true from url link4 votes
- Mar 7, 2023com.microsoft.playwright.PlaywrightException: Host system is missing dependencies to run browsers1 vote
- Jun 14, 2023playwright-java inside Docker RuntimeException: Failed to create driver0 votes · 2 answers
- Nov 13, 2022Java Playwright get response body of redirect0 votes · 1 answer
1 more on Stack Overflow
- Feb 21, 2021Gson Failing to call custom Serializer0 votes
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,542 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository4 other files it gives its agents.
- AGENTS.mdAGENTS.md
- playwright-java-releaseSkill
- playwright-rollSkill
- playwright-triageSkill
The file
We split this AGENTS.md into the rules it gives an agent
4 rules. About 0 are common, 1 is distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 14
Never change a port to make it pass: No skip, condition, wait or assertion that upstream does not have, for any reason: not a browser …
- Very specific: line 40
Every upstream test has an identifier, the first token of its title (it('CH-001 should check the box', ...)).
- Very specific: line 143
Do not add @Fixme, @Disabled or an assumption that upstream does not have, except the @Fixme of the rule at the top.
- Very specific: line 226
When the pending call is an evaluate whose promise upstream awaits later (
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
How to port the upstream tests to Java
These rules say how to write playwright/src/test/java/com/microsoft/playwright/tests/{page,library}
from tests/page and tests/library in microsoft/playwright (the checkout in ../playwright;
the commit the port is based on is in upstream_base.txt, together with the commands that list
the upstream changes still to port). Use them for every new or changed upstream test.
The port is line by line, with zero deviations. A reviewer reads the .spec.ts file and
the .java file side by side. Each upstream statement maps to one Java statement, or to one of
the fixed patterns in this document. The test does what the upstream test does, in the same
order, with the same values, and checks the same things. Port from the upstream source in front
of you, never from memory of what the test does.
Never change a port to make it pass. No skip, condition, wait or assertion that upstream
does not have, for any reason: not a browser channel, not a CI image, not an operating system,
not flakiness. A port that fails shows a bug in the Java client, the driver, the test harness or
the environment: first re-read the upstream test and check that the port is faithful, then
report it with the evidence. The one allowed mark is @Fixme("java: <reason>") for a bug or gap
of the Java client that is not fixed yet, @Fixme("harness: <reason>") for a missing piece of
the test harness, or @Fixme("driver: <reason>") when the upstream test is newer than the driver
this repo bundles (the upstream checkout is ahead of the driver; name the upstream commit or PR
that the driver lacks). The test still runs, its failure is reported as skipped with the reason,
and it fails once it starts to pass so the mark gets removed. @Fixme(value = "...",
browsers = WEBKIT) limits the expectation to some browsers; in a @ParameterizedClass,
parameters = {"type=null,"} limits it to the invocations whose display name contains a
fragment. A test that fails only sometimes is reported as flaky, not marked.
The harness (fixtures, test server, helpers) is com.microsoft.playwright.fixtures, see its
package-info.java.
1. Files and names
- The upstream file decides the Java file:
tests/page/page-check.spec.tsiscom/microsoft/playwright/tests/page/TestPageCheck.java;tests/library/chromium/oopif.spec.tsistests/library/chromium/TestOopif.java. The class name isTestand the file name in PascalCase with-and.as the word separators:elementhandle-clickisTestElementhandleClick,page-clock.frozenisTestPageClockFrozen,matchers.miscisTestMatchersMisc. The class javadoc is/** Port of tests/page/page-check.spec.ts. */; the license header is the one of the upstream file. The tests keep the upstream order. Every upstream test has an identifier, the first token of its title (
it('CH-001 should check the box', ...)).playwright/src/test/upstream_registry.mdis a copy of upstream'stests/registry.mdat the commit inupstream_base.txtand lists them all. The port is a@PlaywrightTestmethod with@DisplayNameholding the upstream title verbatim, identifier and@smoketag included, and a method name that is the title in camelCase without the identifier and tags:@PlaywrightTest @DisplayName("CH-001 should check the box @smoke") void shouldCheckTheBox(Page page) {Characters that cannot be in an identifier are dropped (
should work with $evalisshouldWorkWithEval,toHaveCount passistoHaveCountPass); a title ending in a number keeps it (should throw when not a checkbox 2isshouldThrowWhenNotACheckbox2). Method names must be unique in their class. The@DisplayNameis the title of theit()call itself, with its characters as in the source ("EH-036 should work with <select/>", a"escaped as\"): the registry shows the same title after the describe titles and a›, with markdown escapes, which are not part of it.test.describe('toHaveCount', () => { ... })is a@Nested class ToHaveCount { ... }, named after the describe title in PascalCase. A skip ortest.use()at the top of the describe goes on the nested class (section 3).- A test that a loop declares several times (
for (const kind of kinds) it(...)) is a@Nested @ParameterizedClasswith a@ValueSource/@MethodSourceand a@Parameterfield, holding the test; the@DisplayNamekeeps the title as written in the source, with${kind}. A@Fixturemethod of the nested class may read the field. beforeEach/afterEachare@BeforeEach/@AfterEachmethods, which may take fixture parameters. The fixtures that a file defines (test.extend({...})) are@Fixturemethods at the top of the class returning the fixture's type (a small nested class when upstream returns a plain object). Helper functions of the file are private static methods of the class.- A test that can not exist in Java (a JavaScript-only API, Node.js client internals, tools that
run in the driver,
@playwright/testfeatures) is not ported: it goes inplaywright/src/test/upstream_not_ported.txt, with the reason. Nothing else is left out.python3 scripts/check_test_registry.pychecks that every test in the registry is ported or listed as not ported, under the expected class and title.--missing <code or file glob>prints the names still to port.
2. Fixtures
Fixtures are parameters of the test method (or of a @BeforeEach/@Fixture method), resolved
by type. The upstream fixtures map to:
| Upstream | Java parameter |
page, context, browser, browserType, playwright | Page page, BrowserContext context, Browser browser, BrowserType browserType, Playwright playwright |
browserName | BrowserName browserName (enum: CHROMIUM, FIREFOX, WEBKIT; browserName.toString() is "chromium") |
server, httpsServer | TestServer server, HttpsTestServer httpsServer |
asset('file') | asset("file") (static import of Utils), a Path |
testInfo, test.info() | TestInfo testInfo: outputPath(...), expectToMatchSnapshot(...) |
contextFactory(options) | ContextFactory contextFactory: contextFactory.newContext(options) |
launchPersistent(options) | LaunchPersistent launchPersistent: launchPersistent.launch(options) has .context and .page |
createUserDataDir() | CreateUserDataDir createUserDataDir: createUserDataDir.create() |
pageWithHar(options) | PageWithHar pageWithHar: pageWithHar.create(options) has .page, .context, .getLog(), .getZip() |
channel, headless, isBidi, platform, isWindows, isMac, isLinux, macVersion | Env.channel(), !Env.headed(), Env.isBidi(), Env.platform(), Env.isWindows, Env.isMac, Env.isLinux, Env.macVersion() |
isHeadlessShell | Env.isHeadlessShell(browserName) |
browserVersion, browserMajorVersion | browser.version(), browserMajorVersion(browser) (Utils) |
isAndroid, isElectron, mode !== 'default', isFrozenWebkit | always false: a skip on them is dropped |
toImpl, startRemoteServer, proxyServer, socksPort, childProcess, daemonProcess, waitForPort, findFreePort | not available yet: list the test as not ported with TODO harness: needs <fixture> |
kTargetClosedErrorMessage | kTargetClosedErrorMessage (Utils) |
attachFrame, detachFrame, verifyViewport, expectedSSLError, chromiumVersionLessThan, rafraf, roundBox, unshift | the same names in Utils (static import) |
expect(received).toContainYaml(expected) | expectToContainYaml(received, expected) (Utils) |
expect(buffer).toMatchSnapshot('x.png'), comparePNGs | testInfo.expectToMatchSnapshot(buffer, "x.png"); comparePNGs is a TODO harness |
Upstream's browserType.launch(options) and browserType.launchPersistentContext(dir, options)
add the project's launch options (headless, channel) to the given ones. In Java, build the
options on Env.launchOptions() or Env.launchPersistentContextOptions():
browserType.launch({ proxy: { server } }) is
browserType.launch(Env.launchOptions().setProxy(server)). launchPersistent.launch(options)
does this itself. Never mutate the BrowserType.LaunchOptions fixture: it is shared by the
worker.
TestServer has the methods of upstream's TestServer: PREFIX, CROSS_PROCESS_PREFIX,
EMPTY_PAGE, HOST, HOSTNAME, PORT, setRoute, setRedirect, setAuth, setCSP,
setExtraHeaders, enableGzip, setContent, serveFile, waitForRequest,
onceWebSocketConnection, waitForWebSocket, waitForWebSocketConnectionRequest,
waitForUpgrade, sendOnWebSocketConnection. A route handler is
(req, res) -> { ... } with the Node shapes: req.url, req.method, req.headers (lower-case
names), req.rawHeaders, req.postBody(), req.socket().destroy(); res.writeHead(200,
"Content-Type", "text/html") (names and values alternate; or a Map), res.setHeader,
res.statusCode = 404, res.write, res.end, res.flushHeaders(), res.socket(). A handler
that upstream writes as () => {} (never answers) is (req, res) -> {}.
await server.waitForRequest('/x') is server.waitForRequest("/x").get(); create the future
before the action that triggers the request, like upstream. The future completes after the route
handler of the request has returned, as in the Node.js server, so a response stored by the handler
((req, res) -> serverResponse[0] = res) is set when get() returns. A server WebSocket (ws of
onceWebSocketConnection) has send, close(code, reason), onMessage(message -> ...)
(message.text(), message.data, message.isBinary), onClose((code, reason) -> ...),
waitForMessage(), waitForClose().
3. Skips and marks
it.skip(browserName === 'webkit', reason)is@DisabledOnBrowser(value = WEBKIT, reason = "...");browserName === 'webkit' && isMacis@DisabledOnBrowser(value = WEBKIT, os = OS.MAC);!headlessaddsheaded = true.it.skip(browserName !== 'chromium')is@EnabledOnBrowser(CHROMIUM). Any other condition isassumeFalse(condition, reason)/assumeTrue(...)(org.junit.jupiter.api.Assumptions) at the top of the test, with the same condition. A skip at the top of a file or a describe is the annotation on the class.it.fixme(condition, reason)is the same skip with the reason prefixedfixme:.it.fail(...)is the same skip with the reason prefixedfail:. A skip upstream is a skip in the port; it is not a deviation.test.slow(),test.setTimeout(...),test.info().annotationsand{ annotation: ... }are not ported.test.step('name', async () => { ... })is its body, after a// it.step('name')comment.- Do not add
@Fixme,@Disabledor an assumption that upstream does not have, except the@Fixmeof the rule at the top.
4. Statements
expect(a).toBe(b)andtoEqual(b)areassertEquals(b, a): the expected value comes first.toBeTruthy()/toBeFalsy()areassertTrue/assertFalse(orassertNotNull/assertNullfor an object);toContain(x)isassertTrue(a.contains(x));toHaveLength(n)isassertEquals(n, a.size());toBeGreaterThan(n)isassertTrue(a > n);toMatch(/re/)isassertTrue(Pattern.compile("re").matcher(a).find());toBeInstanceOf(X)isassertTrue(a instanceof X). A matcher on a locator, page or response (await expect(locator).toHaveText(...)) is the same matcher of the Java API:assertThat(locator).hasText(...)(PlaywrightAssertions.assertThat),.not.is.not().await expect(promise).rejects.toThrow('text')andconst error = await promise.catch(e => e); expect(error.message).toContain('text')arePlaywrightException error = assertThrows(PlaywrightException.class, () -> ...)thenassertTrue(error.getMessage().contains("text")). The class is the one upstream expects:TimeoutErrorforplaywright.errors.TimeoutError,TargetClosedError(com.microsoft.playwright.impl) forTargetClosedError. Compare messages withcontains, as upstream does; the Java message of a driver error is the serialized error, which contains the message text but not thepage.goto:prefix or the call log of the Node.js client: a test that checks those gets@Fixme("java: ...").const [popup] = await Promise.all([page.waitForEvent('popup'), page.click('a')])isPage popup = page.waitForPopup(() -> page.click("a")); the same forwaitForRequest,waitForResponse,waitForNavigation,waitForDownload,waitForFileChooser,waitForConsoleMessage,waitForWebSocket,waitForWorker,context.waitForPageandwaitForEvent('x', predicate)(page.waitForX(predicate, () -> ...)). The promises after the first go in the callback, in order.page.on('x', ...)ispage.onX(...),page.offispage.offX;page.once('x', h)is anonXhandler that callsoffXon itself first.- The Java API is synchronous and a Playwright instance is used from one thread only: the
test never calls it from another thread (no
Thread,CompletableFuture.supplyAsyncor executor). A promise that is created before and awaited later is ported in the first of these ways that applies:- The callback form of the wait (previous bullet), when everything up to the
awaitcan go in the callback. - A wait on a state that stays satisfied once reached (
waitForSelector,waitForFunction,locator.waitFor,waitForElementState,waitForLoadState,waitForURL, anexpect(locator)matcher,response.finished(),download.path(),page.waitForTimeout): the actions first, the wait after them.const w = page.waitForSelector('div'); await page.setContent('<div>'); await wispage.setContent("<div>"); page.waitForSelector("div"). APromise.allof such calls, or of one blocking call and others that do not block, runs them one after the other. - A call that waits only for the test server (
const p = page.goto(url); const req = await server.waitForRequest(path); ...; res.end(); await p): the server side goes in the route handler, which the server runs on a thread of its own (server.setRoute(path, (req, res) -> { ...; res.end(); })), and the call is made inline afterwards; the checks on the request come after the call, from theserver.waitForRequestfuture. A handler may sleep or wait on aCompletableFuturethat the test completes, as long as the test does not need Playwright for that. Agotothat upstream leaves pending while it waits fordomcontentloadedwith another call is anavigatewithsetWaitUntil(DOMCONTENTLOADED), inside the outer wait when there is one (page.waitForNavigation(load, () -> { page.waitForNavigation(domcontentloaded, () -> page.navigate(url, domcontentloaded)); ...; release.complete(null); })); agotothat must not resolve (expect(done).toBe(false)after a while) is anavigatewith a short timeout that is expected to throwTimeoutError. - A call that upstream does not await (
page.evaluate(...).catch(() => {}),void page.evaluate(...)) and that only starts work in the page: the page function starts the work without returning its promise (fetch(url).catch(() => {})instead ofreturn fetch(url)), and keeps the promise onwindowwhen the result is needed later (window.__status = fetch(url).then(r => r.status), read with a secondpage.evaluate("() => window.__status"), which awaits it). A step that upstream takes while the call is pending and that is itself page work (apushState, a reload, a sync XHR) is scheduled from the same function withsetTimeout. A route that the handler keeps pending (page.route(url, route -> routes[0] = route)) is awaited withpage.waitForCondition(() -> routes[0] != null); an event without a callback wait (requestfailed) is a handler that stores it, awaited the same way. - A wait that upstream rejects by closing (
page.waitForEvent('download')pending, thenbrowser.close()orremoteServer.close()): the close goes in the callback of the wait, which then throws (assertThrows(..., () -> page.waitForDownload(() -> browser.close()))); two such waits nest, with the close in the innermost callback. A close while a route handler holds the request (page.gotopending, the handler called, thenpage.close()): the close goes inside the handler (page.route(p, route -> { page.close(); route.resume(); })), which runs on the test thread inside the navigation. A navigation that upstream starts without awaiting, to do something while it is stalled (page.goto(stall).catch(() => {}), thenpage.click('body')): the navigation is started from the page (page.evaluate("url => { window.location.href = url; }", url)) and the stall is timed in the route handler (Thread.sleep(1000); req.socket().destroy();). - Otherwise the test needs a Playwright call while another one is pending (an assertion on
the pending state such as
expect(done).toBe(false)while a click waits, anevaluatewhile agotois stalled, acloseracing a call), which the synchronous API can not do: the test is not ported. Delete it and add it toupstream_not_ported.txtwith the reasonjava: concurrent: <call> while <call> is pending, for exampleCL-022 java: concurrent: page.$eval while page.click is pending. When the pending call is anevaluatewhose promise upstream awaits later (const p = page.evaluate(() => new Promise(...)); ...; await p) and that evaluate is only a vehicle for the test, not the thing it checks, the test waits for a rewrite upstream instead: the reason isupstream: evaluate promise awaited later, waits for the rewrite in upstream_rewrites/pending_evaluate_promise.md, and the test is added to that file, the prompt for rewriting it upstream so that the promise is kept onwindowand awaited by a secondevaluate, or so that awaitForEventspans the other calls. A test whose point is that the pending call gets rejected when its page, frame or browser goes away staysjava: concurrent.
- The callback form of the wait (previous bullet), when everything up to the
- A promise settled by an event handler (
new Promise(f => page.once('load', f)), awaited later) is aCompletableFuturethat theonLoadhandler completes, awaited withpage.waitForCondition(future::isDone): the Java client only delivers events while the test thread is inside a Playwright call, sofuture.get()would hang forever. page.evaluate(() => ...): the function stays JavaScript, in a string, with the same body. A function with arguments takes one argument:page.evaluate("([a, b]) => a + b", asList(a, b))orpage.evaluate("x => x", 7). A Node.js value that goes to the page is the Java value:undefinedandnullarenull, an object is aMap(mapOf("k", v, ...)inUtils), an array is aList(asList), aBufferisbyte[], aRegExpis aPattern, aDateis aDate. A value from the page is aString, aBoolean, anInteger(a whole number) or aDouble, aList<Object>, aMap<String, Object>ornull:expect(await page.evaluate(() => 7)).toBe(7)isassertEquals(7, page.evaluate("() => 7")), an object isassertEquals(mapOf("foo", "bar"), ...), an array isassertEquals(asList(1, 2), ...).- The Java API names:
page.gotoispage.navigate,page.$isquerySelector,page.$$isquerySelectorAll,page.$evalisevalOnSelector,page.$$evalisevalOnSelectorAll,page.mouseispage.mouse(),page.keyboardispage.keyboard(),page.clockispage.clock(),browser.newPage()and the rest are the same. Options are the options object of the call:{ waitUntil: 'load', timeout: 5000 }isnew Page.NavigateOptions().setWaitUntil(WaitUntilState.LOAD).setTimeout(5000). The signatures are inplaywright/src/main/java/com/microsoft/playwright/*.java; read the file rather than guessing a name. The keys of the data that the page or the driver returns stay as they are (request.headers().get("content-type")). new Promise(resolve => ...)that the test settles itself is aCompletableFuturethat the test completes;new Promise(f => setTimeout(f, 100))ispage.waitForTimeout(100)when a page is at hand, elseThread.sleep(100).console.logand comments stay, as comments. A comment that explains an upstream workaround stays with the code it explains.- The template strings of
setContentkeep their newlines and indentation, as"...\n" +concatenation.
5. Marks
The port has one mark of its own, // Java:, on the line before a statement whose form the
rules above change in a way that is not obvious: a value that Java can not express as upstream
does, a helper that stands for a JavaScript construct. Do not mark a plain rename or a plain
reorder. A mark is not a deviation: the test still does what upstream does.
6. Running
From playwright/: mvn -o -q test -Dtest=TestPageCheck runs the class in the three browsers
(BROWSER=chromium,firefox to pick some). The reports are in target/surefire-reports,
TEST-*.xml names each test case after the upstream title. mvn -o -q test-compile only
compiles. The tests that launch a headed browser (TestHeadful and the headed = true
projects) run under a virtual display on Linux, as upstream does on CI:
xvfb-run -a --server-args="-screen 0 1280x960x24" mvn -o test ...; on the real display of a
desktop session the headed WebKit tests hang. Put a timeout in front of a long run so that a
hang does not stall it.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

