diff --git a/playwright/src/main/java/com/microsoft/playwright/Frame.java b/playwright/src/main/java/com/microsoft/playwright/Frame.java index 29198dd09..a8b5e7b1d 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Frame.java +++ b/playwright/src/main/java/com/microsoft/playwright/Frame.java @@ -5457,5 +5457,11 @@ default void waitForURL(Predicate url) { * @since v1.11 */ void waitForURL(Predicate url, @Nullable WaitForURLOptions options); + /** + * Tools that the frame registers through the experimental {@code WebMCP} browser API. See {@code WebMCP} for details. + * + * @since v1.64 + */ + WebMCP webmcp(); } diff --git a/playwright/src/main/java/com/microsoft/playwright/Locator.java b/playwright/src/main/java/com/microsoft/playwright/Locator.java index 25e0f1740..ff57fa4e4 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Locator.java +++ b/playwright/src/main/java/com/microsoft/playwright/Locator.java @@ -4610,6 +4610,21 @@ default Locator locator(Locator selectorOrLocator) { * and other user-facing attributes over CSS selectors. This is useful for converting implementation-detail selectors into * more resilient, human-readable locators. * + *

Usage + * + *

An agent can pick an element by its ref from an {@link com.microsoft.playwright.Page#ariaSnapshot Page.ariaSnapshot()} + * taken in the {@code "ai"} mode. Refs only work for the latest snapshot. Normalize the ref locator and convert it to a + * string to get resilient locator code in your language, the same way {@code codegen} does. + * + *

Consider the following aria snapshot. + * + *

You can turn the ref into locator code: + *

{@code
+   * Locator normalized = page.getByRef("e2").normalize();
+   * System.out.println(normalized.toString());
+   * // getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Submit"))
+   * }
+ * * @since v1.59 */ Locator normalize(); diff --git a/playwright/src/main/java/com/microsoft/playwright/Page.java b/playwright/src/main/java/com/microsoft/playwright/Page.java index 9ac054a20..f342efef6 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Page.java +++ b/playwright/src/main/java/com/microsoft/playwright/Page.java @@ -8967,6 +8967,16 @@ default Worker waitForWorker(Runnable callback) { * @since v1.9 */ Worker waitForWorker(@Nullable WaitForWorkerOptions options, Runnable callback); + /** + * Tools that the main frame registers through the experimental {@code WebMCP} browser API. Shortcut for {@link + * com.microsoft.playwright.Frame#webmcp Frame.webmcp()} of {@link com.microsoft.playwright.Page#mainFrame + * Page.mainFrame()}, see {@code WebMCP} for details. + * + *

Usage + * + * @since v1.64 + */ + WebMCP webmcp(); /** * This method returns all of the dedicated WebWorkers associated with the page. diff --git a/playwright/src/main/java/com/microsoft/playwright/WebMCP.java b/playwright/src/main/java/com/microsoft/playwright/WebMCP.java new file mode 100644 index 000000000..6752825a7 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/WebMCP.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.playwright; + +import org.jspecify.annotations.Nullable; +import com.microsoft.playwright.options.*; +import java.util.*; + +/** + * {@code WebMCP} exposes the tools that a frame registers through the experimental {@code WebMCP} browser API, {@code + * navigator.modelContext}. It lists the tools and calls them. + * + *

Instances are accessed through {@link com.microsoft.playwright.Frame#webmcp Frame.webmcp()}. {@link + * com.microsoft.playwright.Page#webmcp Page.webmcp()} is the instance of the main frame. + * + *

NOTE: WebMCP is an experimental browser feature. Chromium enables it with the {@code --enable-features=WebMCP} launch + * argument, Firefox with the {@code dom.modelcontext.enabled} and {@code dom.modelcontext.testing.enabled} preferences. + * WebKit does not implement it. + * + *

Tool names, descriptions, input schemas and results are provided by the page, so treat them as untrusted input. + */ +public interface WebMCP { + class CallToolOptions { + /** + * Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default + * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout + * BrowserContext.setDefaultTimeout()} or {@link com.microsoft.playwright.Page#setDefaultTimeout Page.setDefaultTimeout()} + * methods. + */ + public @Nullable Double timeout; + + /** + * Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default + * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout + * BrowserContext.setDefaultTimeout()} or {@link com.microsoft.playwright.Page#setDefaultTimeout Page.setDefaultTimeout()} + * methods. + */ + public CallToolOptions setTimeout(double timeout) { + this.timeout = timeout; + return this; + } + } + class ToolsOptions { + /** + * Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default + * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout + * BrowserContext.setDefaultTimeout()} or {@link com.microsoft.playwright.Page#setDefaultTimeout Page.setDefaultTimeout()} + * methods. + */ + public @Nullable Double timeout; + + /** + * Maximum time in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The default + * value can be changed by using the {@link com.microsoft.playwright.BrowserContext#setDefaultTimeout + * BrowserContext.setDefaultTimeout()} or {@link com.microsoft.playwright.Page#setDefaultTimeout Page.setDefaultTimeout()} + * methods. + */ + public ToolsOptions setTimeout(double timeout) { + this.timeout = timeout; + return this; + } + } + /** + * Calls a tool registered by the frame and returns its result. The result is whatever the tool's {@code execute} function + * resolved to, typically an object with a {@code content} array. A result with {@code isError: true} is returned as is. + * The method throws when the tool is not registered or its {@code execute} function throws. + * + * @param name Name of the tool, as reported by {@link com.microsoft.playwright.WebMCP#tools WebMCP.tools()}. + * @param input Input for the tool, matching its {@code inputSchema}. Defaults to an empty object. + * @since v1.64 + */ + default @Nullable Object callTool(String name, @Nullable Object input) { + return callTool(name, input, null); + } + /** + * Calls a tool registered by the frame and returns its result. The result is whatever the tool's {@code execute} function + * resolved to, typically an object with a {@code content} array. A result with {@code isError: true} is returned as is. + * The method throws when the tool is not registered or its {@code execute} function throws. + * + * @param name Name of the tool, as reported by {@link com.microsoft.playwright.WebMCP#tools WebMCP.tools()}. + * @since v1.64 + */ + default @Nullable Object callTool(String name) { + return callTool(name, null); + } + /** + * Calls a tool registered by the frame and returns its result. The result is whatever the tool's {@code execute} function + * resolved to, typically an object with a {@code content} array. A result with {@code isError: true} is returned as is. + * The method throws when the tool is not registered or its {@code execute} function throws. + * + * @param name Name of the tool, as reported by {@link com.microsoft.playwright.WebMCP#tools WebMCP.tools()}. + * @param input Input for the tool, matching its {@code inputSchema}. Defaults to an empty object. + * @since v1.64 + */ + @Nullable Object callTool(String name, @Nullable Object input, @Nullable CallToolOptions options); + /** + * Returns the tools currently registered by the frame. Throws if the browser was launched without WebMCP support, see the + * note above for the launch options that enable it. + * + *

{@link com.microsoft.playwright.Page#webmcp Page.webmcp()} covers the main frame only, child frames list their tools + * through their own {@link com.microsoft.playwright.Frame#webmcp Frame.webmcp()}. + * + * @since v1.64 + */ + default List tools() { + return tools(null); + } + /** + * Returns the tools currently registered by the frame. Throws if the browser was launched without WebMCP support, see the + * note above for the launch options that enable it. + * + *

{@link com.microsoft.playwright.Page#webmcp Page.webmcp()} covers the main frame only, child frames list their tools + * through their own {@link com.microsoft.playwright.Frame#webmcp Frame.webmcp()}. + * + * @since v1.64 + */ + List tools(@Nullable ToolsOptions options); +} + diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java index 330d6698a..6afe23580 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/FrameImpl.java @@ -47,6 +47,7 @@ enum InternalEventType { NAVIGATED, LOADSTATE } private final ListenerCollection internalListeners = new ListenerCollection<>(); PageImpl page; boolean isDetached; + private final WebMCPImpl webmcp = new WebMCPImpl(this); FrameImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { super(parent, type, guid, initializer); @@ -1137,6 +1138,11 @@ protected void handleEvent(String event, JsonObject params) { } } + @Override + public WebMCP webmcp() { + return webmcp; + } + protected double timeout(Double timeout) { if (page != null) { return page.timeoutSettings.timeout(timeout); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/LocatorImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/LocatorImpl.java index 719b969c8..cb4357846 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/LocatorImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/LocatorImpl.java @@ -35,6 +35,7 @@ class LocatorImpl implements Locator { final FrameImpl frame; final String selector; + private String stringRepresentation; LocatorImpl(FrameImpl frame, String selector, LocatorOptions options) { this(frame, selector, options, null); @@ -113,8 +114,13 @@ public List allTextContents() { public Locator normalize() { JsonObject params = new JsonObject(); params.addProperty("selector", selector); + params.addProperty("sdkLanguage", "java"); JsonObject result = frame.sendMessage("resolveSelector", params, ChannelOwner.NO_TIMEOUT).getAsJsonObject(); - return new LocatorImpl(frame, result.get("resolvedSelector").getAsString(), null); + LocatorImpl locator = new LocatorImpl(frame, result.get("resolvedSelector").getAsString(), null); + if (result.has("locatorCode")) { + locator.stringRepresentation = result.get("locatorCode").getAsString(); + } + return locator; } @Override @@ -689,6 +695,9 @@ public Locator within(Locator locator) { @Override public String toString() { + if (stringRepresentation != null) { + return stringRepresentation; + } String description = description(); if (description != null) { return description; diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java index 80205589b..218fa80f1 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java @@ -1088,6 +1088,11 @@ public Frame mainFrame() { return mainFrame; } + @Override + public WebMCP webmcp() { + return mainFrame.webmcp(); + } + @Override public void hideHighlight() { sendMessage("hideHighlight", new JsonObject(), NO_TIMEOUT); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/WebMCPImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/WebMCPImpl.java new file mode 100644 index 000000000..1cb76bef1 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/WebMCPImpl.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) Microsoft Corporation. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * http://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.playwright.impl; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.microsoft.playwright.WebMCP; +import com.microsoft.playwright.options.WebMCPTool; + +import java.util.Arrays; +import java.util.List; + +import static com.microsoft.playwright.impl.Serialization.deserialize; +import static com.microsoft.playwright.impl.Serialization.gson; +import static com.microsoft.playwright.impl.Serialization.serializeArgument; + +class WebMCPImpl implements WebMCP { + private final FrameImpl frame; + + WebMCPImpl(FrameImpl frame) { + this.frame = frame; + } + + @Override + public List tools(ToolsOptions options) { + JsonElement json = frame.sendMessage("webmcpTools", new JsonObject(), frame.timeout(options == null ? null : options.timeout)); + WebMCPTool[] tools = gson().fromJson(json.getAsJsonObject().get("tools"), WebMCPTool[].class); + return Arrays.asList(tools); + } + + @Override + public Object callTool(String name, Object input, CallToolOptions options) { + JsonObject params = new JsonObject(); + params.addProperty("name", name); + params.add("input", gson().toJsonTree(serializeArgument(input))); + JsonElement json = frame.sendMessage("webmcpCallTool", params, frame.timeout(options == null ? null : options.timeout)); + SerializedValue value = gson().fromJson(json.getAsJsonObject().get("result"), SerializedValue.class); + return deserialize(value); + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/options/WebMCPTool.java b/playwright/src/main/java/com/microsoft/playwright/options/WebMCPTool.java new file mode 100644 index 000000000..8bb6fecee --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/options/WebMCPTool.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.playwright.options; + +import org.jspecify.annotations.Nullable; + +public class WebMCPTool { + /** + * Tool name, unique within the frame. + */ + public String name; + /** + * Tool description. + */ + public String description; + /** + * JSON Schema of the tool input, when the page provides one. + */ + public @Nullable Object inputSchema; + /** + * Hints the page provides about the tool. + */ + public @Nullable WebMCPToolAnnotations annotations; + +} \ No newline at end of file diff --git a/playwright/src/main/java/com/microsoft/playwright/options/WebMCPToolAnnotations.java b/playwright/src/main/java/com/microsoft/playwright/options/WebMCPToolAnnotations.java new file mode 100644 index 000000000..b991f5160 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/options/WebMCPToolAnnotations.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.playwright.options; + +import org.jspecify.annotations.Nullable; + +public class WebMCPToolAnnotations { + /** + * The tool does not modify any state. + */ + public @Nullable Boolean readOnly; + /** + * The tool output may contain third-party content. + */ + public @Nullable Boolean untrustedContent; + /** + * The tool takes a consequential action, such as placing an order. + */ + public @Nullable Boolean consequential; + + /** + * The tool does not modify any state. + */ + public WebMCPToolAnnotations setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + return this; + } + /** + * The tool output may contain third-party content. + */ + public WebMCPToolAnnotations setUntrustedContent(boolean untrustedContent) { + this.untrustedContent = untrustedContent; + return this; + } + /** + * The tool takes a consequential action, such as placing an order. + */ + public WebMCPToolAnnotations setConsequential(boolean consequential) { + this.consequential = consequential; + return this; + } +} \ No newline at end of file diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageAriaSnapshotAI.java b/playwright/src/test/java/com/microsoft/playwright/TestPageAriaSnapshotAI.java index 1dad46042..233ba61e3 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageAriaSnapshotAI.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageAriaSnapshotAI.java @@ -191,4 +191,17 @@ void shouldLimitDepth(Page page) { assertTrue(snapshot4.contains("listitem [ref=e7]: item2"), snapshot4); assertFalse(snapshot4.contains("item3"), snapshot4); } + + @Test + void shouldNormalizeRefLocatorToLocatorCode(Page page) { + page.setContent(""); + String snapshot = aiSnapshot(page); + assertTrue(snapshot.contains("button \"Submit\" [ref=e2]"), snapshot); + + Locator normalized = page.getByRef("e2").normalize(); + assertEquals("getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(\"Submit\"))", normalized.toString()); + + PlaywrightException e = assertThrows(PlaywrightException.class, () -> page.getByRef("e1000").normalize()); + assertTrue(e.getMessage().contains("No element matching aria-ref=e1000"), e.getMessage()); + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestWebMCP.java b/playwright/src/test/java/com/microsoft/playwright/TestWebMCP.java new file mode 100644 index 000000000..6fd33967f --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestWebMCP.java @@ -0,0 +1,179 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.playwright; + +import com.microsoft.playwright.options.WebMCPTool; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIf; + +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.Comparator; +import java.util.List; + +import static com.microsoft.playwright.Utils.assertJsonEquals; +import static com.microsoft.playwright.Utils.mapOf; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; + +@DisabledIf(value="com.microsoft.playwright.TestBase#isWebKit", disabledReason="WebKit does not implement WebMCP") +public class TestWebMCP extends TestBase { + private static final String ADD_TOOL = "modelContext.registerTool({\n" + + " name: 'add',\n" + + " description: 'Adds two numbers',\n" + + " inputSchema: { type: 'object', properties: { a: { type: 'number' }, b: { type: 'number' } }, required: ['a', 'b'] },\n" + + " annotations: { readOnlyHint: true },\n" + + " async execute(input) { return { content: [{ type: 'text', text: String(input.a + input.b) }] }; },\n" + + "});\n"; + + @Override + @BeforeAll + void launchBrowser() { + BrowserType.LaunchOptions options = createLaunchOptions(); + if (isFirefox()) { + options.setFirefoxUserPrefs(mapOf( + "dom.modelcontext.enabled", true, + "dom.modelcontext.testing.enabled", true)); + } else { + options.setArgs(singletonList("--enable-features=WebMCP")); + } + launchBrowser(options); + } + + private static String registerScript(String tools) { + return ""; + } + + private void serve(String path, String body) { + server.setRoute(path, exchange -> { + exchange.getResponseHeaders().add("Content-Type", "text/html"); + exchange.sendResponseHeaders(200, 0); + try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) { + writer.write("WebMCP" + body); + } + }); + } + + @Test + void shouldListToolsRegisteredByThePage() { + serve("/", registerScript(ADD_TOOL + + "modelContext.registerTool({\n" + + " name: 'noop',\n" + + " description: 'Does nothing',\n" + + " async execute() {},\n" + + "});\n")); + page.navigate(server.PREFIX + "/"); + + List tools = page.webmcp().tools(); + // Browsers list tools in their own order. + tools.sort(Comparator.comparing(tool -> tool.name)); + assertJsonEquals("[{\n" + + " \"name\": \"add\",\n" + + " \"description\": \"Adds two numbers\",\n" + + " \"inputSchema\": { \"type\": \"object\", \"properties\": { \"a\": { \"type\": \"number\" }, \"b\": { \"type\": \"number\" } }, \"required\": [\"a\", \"b\"] },\n" + + " \"annotations\": { \"readOnly\": true }\n" + + "}, {\n" + + " \"name\": \"noop\",\n" + + " \"description\": \"Does nothing\"\n" + + "}]", tools); + assertTrue(tools.get(0).annotations.readOnly); + } + + @Test + void shouldReportNoToolsWhenThePageRegistersNone() { + page.navigate(server.EMPTY_PAGE); + assertEquals(0, page.webmcp().tools().size()); + } + + @Test + void shouldCallATool() { + serve("/", registerScript(ADD_TOOL)); + page.navigate(server.PREFIX + "/"); + + Object result = page.webmcp().callTool("add", mapOf("a", 2, "b", 40)); + assertEquals(mapOf("content", asList(mapOf("type", "text", "text", "42"))), result); + } + + @Test + void shouldReturnIsErrorResultsAsIs() { + serve("/", registerScript( + "modelContext.registerTool({\n" + + " name: 'broken',\n" + + " description: 'Always fails',\n" + + " async execute() { return { content: [{ type: 'text', text: 'nope' }], isError: true }; },\n" + + "});\n")); + page.navigate(server.PREFIX + "/"); + + Object result = page.webmcp().callTool("broken"); + assertEquals(mapOf("content", asList(mapOf("type", "text", "text", "nope")), "isError", true), result); + } + + @Test + void shouldThrowForAnUnknownTool() { + serve("/", registerScript(ADD_TOOL)); + page.navigate(server.PREFIX + "/"); + + PlaywrightException e = assertThrows(PlaywrightException.class, () -> page.webmcp().callTool("missing")); + assertTrue(e.getMessage().contains("No WebMCP tool named \"missing\""), e.getMessage()); + assertTrue(e.getMessage().contains("Available tools: add"), e.getMessage()); + } + + @Test + void shouldTimeOutWhenTheToolDoesNotSettle() { + serve("/", registerScript( + "modelContext.registerTool({\n" + + " name: 'stuck',\n" + + " description: 'Never resolves',\n" + + " execute() { return new Promise(() => {}); },\n" + + "});\n")); + page.navigate(server.PREFIX + "/"); + + TimeoutError e = assertThrows(TimeoutError.class, () -> page.webmcp().callTool("stuck", mapOf(), new WebMCP.CallToolOptions().setTimeout(500))); + assertTrue(e.getMessage().contains("Timeout 500ms exceeded"), e.getMessage()); + } + + @Test + @DisabledIf(value="com.microsoft.playwright.TestBase#isFirefox", disabledReason="Firefox does not support registering WebMCP tools in iframes yet") + void shouldScopeToolsToTheirFrame() { + serve("/", registerScript(ADD_TOOL) + ""); + serve("/frame.html", registerScript( + "modelContext.registerTool({\n" + + " name: 'subscribe',\n" + + " description: 'Subscribes to the newsletter',\n" + + " async execute() { return { content: [{ type: 'text', text: 'subscribed' }] }; },\n" + + "});\n")); + page.navigate(server.PREFIX + "/"); + + Frame childFrame = page.frames().get(1); + List mainTools = page.webmcp().tools(); + assertEquals(1, mainTools.size()); + assertEquals("add", mainTools.get(0).name); + List frameTools = childFrame.webmcp().tools(); + assertEquals(1, frameTools.size()); + assertEquals("subscribe", frameTools.get(0).name); + assertEquals(mapOf("content", asList(mapOf("type", "text", "text", "subscribed"))), childFrame.webmcp().callTool("subscribe")); + + PlaywrightException e = assertThrows(PlaywrightException.class, () -> page.webmcp().callTool("subscribe")); + assertTrue(e.getMessage().contains("No WebMCP tool named \"subscribe\""), e.getMessage()); + assertTrue(e.getMessage().contains("Available tools: add"), e.getMessage()); + } +} diff --git a/scripts/DRIVER_VERSION b/scripts/DRIVER_VERSION index 0d79596a3..e4219206d 100644 --- a/scripts/DRIVER_VERSION +++ b/scripts/DRIVER_VERSION @@ -1 +1 @@ -1.64.0-beta-1791325510000 +1.64.0-beta-1791568591000 diff --git a/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java b/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java index 25b29bfab..81e48a0fb 100644 --- a/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java +++ b/tools/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java @@ -1065,7 +1065,7 @@ void writeTo(List output, String offset) { if (asList("Playwright", "FormData", "RequestOptions").contains(jsonName) && methods.stream().anyMatch(m -> "create".equals(m.jsonName))) { output.add("import com.microsoft.playwright.impl." + jsonName + "Impl;"); } - if (asList("Page", "Request", "Response", "APIRequestContext", "APIRequest", "APIResponse", "FileChooser", "Frame", "FrameLocator", "ElementHandle", "Locator", "Browser", "BrowserContext", "BrowserType", "Mouse", "Keyboard", "Tracing", "Video", "Debugger", "Screencast", "WebError", "Credentials", "WebStorage").contains(jsonName)) { + if (asList("Page", "Request", "Response", "APIRequestContext", "APIRequest", "APIResponse", "FileChooser", "Frame", "FrameLocator", "ElementHandle", "Locator", "Browser", "BrowserContext", "BrowserType", "Mouse", "Keyboard", "Tracing", "Video", "Debugger", "Screencast", "WebError", "Credentials", "WebStorage", "WebMCP").contains(jsonName)) { output.add("import com.microsoft.playwright.options.*;"); } if ("Download".equals(jsonName)) { @@ -1077,7 +1077,7 @@ void writeTo(List output, String offset) { if ("Clock".equals(jsonName)) { output.add("import java.util.Date;"); } - if (asList("Page", "Frame", "ElementHandle", "Locator", "LocatorAssertions", "APIRequest", "APIRequestContext", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright", "Debugger", "Screencast", "WebSocketRoute", "Credentials", "WebStorage").contains(jsonName)) { + if (asList("Page", "Frame", "ElementHandle", "Locator", "LocatorAssertions", "APIRequest", "APIRequestContext", "Browser", "BrowserContext", "BrowserType", "Route", "Request", "Response", "JSHandle", "ConsoleMessage", "APIResponse", "Playwright", "Debugger", "Screencast", "WebSocketRoute", "Credentials", "WebStorage", "WebMCP").contains(jsonName)) { output.add("import java.util.*;"); } if (asList("WebSocketRoute").contains(jsonName)) {